home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Hottest 6
/
Hottest 6 (1996)(PDSoft)[!].iso
/
software
/
programming
/
c
/
shadow
/
docs
/
introduction.doc
< prev
next >
Wrap
Text File
|
1978-11-24
|
204KB
|
4,741 lines
Shadow Development Documentation
Library Version 5.0
By David Navas
Updated: 13 Nov 1992
Copyright © 1992 by David Navas
with help from Karen Lien
All Rights Reserved
THE PLAN
Basic Concepts
History
Overview
Resource Tracking
Dual-pass resource tracking
System String Constants
Semaphores
Intra-Process Locking Integrity
Counting Semaphores
Conditional Variables
Multi-Process Signalling
Performance Issues
AvlTrees
Performance Issues
Summary
SLists
Memory
OOPL
Object Oriented
Object-Orientedness under SHADOW
Creating Classes
AttributeTags
MethodTags
mtag_defnObject
mtag_procObject
mtag_flags
mtag_threadstat
mtag_priority
mtag_method
mtag_arguments
ArgumentTags
CreateInstance
Wrap-up of Methods
Patcher Class
Director Class
Process Class
Process and Program Shutdown
Conclusion
BASIC CONCEPTS
SHADOW is a concurrent-object-oriented addition to AmigaDOS.
Its principle design goal is to help standardize an extensible
environment paradigm. It takes advantage of some of the better
AmigaDOS facilities (shared memory system, IPC ports, and fast
context switching) by internally managing much of the inter-task
communications, resource tracking, and resource allocation.
Traditional object-oriented systems separate function
interfaces from internal data structures and manage the allocation
and access of these structures within objects. SHADOW takes this
interface separation one step further by partially uncoupling the
parameter specification at method invocation from the parameter
specification of the method implementation.
Because of this relatively high separation, argument type-
checking is not implemented under SHADOW. To implement
type-checking in a run-time bound environment almost requires
run-time type-checking. It was decided that this was too
expensive a use of computing resources. Under AmigaOS, there is
some precedence for this; for example, it is difficult to type-check
TagItem strutures.
While SHADOW's main goal is to provide a unified interface
for distributed, extensible, multi-threaded computing, there are
many provisions for speed, this being just one of them. Other
examples include callback hooks for method dispatching and attribute
lookups; if you feel SHADOW's versions are not quite what you want
in either speed or features or both, you can set these hooks on a
class-by-class basis. Additionally, attributes are created in such a
manner as to provide multiple ways of getting at object instance
variables -- including (but not limited to) the use of more "normal"
C-structure dereferencing. While this type of dereferencing isn't
guaranteed if you're using someone else's class (or, even, if you
are subclassed off of someone else's class), it does provide a
very fast way of getting at attributes, and SHADOW uses it in
several places within its internal code for the sake of efficiency.
HISTORY
SHADOW was created to solve the problems which I ran into with
my first programming project -- JazzBench. That experience taught
me that the most important thing in a co-operative multi-program
environment is flexibility. You need to be able to change the
behaviour of EVERYTHING -as- -it- -runs-. This lesson was the
principle reason behind the initial design of SHADOW, and the result
of that principle was the entire WatchedVariable construct. To a
lesser extent, it was also responsible for Patches.
However, that was not the only lesson that was learned. Trying
to locate governing control within a single server not only created
bottleneck problems, but also, in the end, either limited or
complicated the design process for my programs. What I needed was a
subsystem which effectively dealt with the concurrency and shared
resource management problems across a distributed environment.
The other major subsystems of SHADOW support the increased use
of more complicated data structures. Exec lists are very fast at
some things, but searching and sorting are definitely not their
strong points.
OVERVIEW
This introduction is an attempt to acquaint the reader with SHADOW's
capabilities and limitations, and to introduce the programming
paradigm it offers to Amiga programmers.
The number of functions in SHADOW is truly daunting. However, only a
very few need to be well-understood in order to program SHADOW
effectively. These functions fall into about seven categories, which
we will take up in turn. The following is an informal introduction
to familiarize you with the terminology and the pertinent function
calls in SHADOW. Confusing terms can (hopefully) be found in
Glossary.doc. If they can't be, please let me know what they are, and
they will appear in future versions of that documentation file.
RESOURCE TRACKING CALLS
SHADOW gives programmers the ability to track resources, and
in fact forces them to do so. Objects are NEVER freed until the
useCount drops to zero. Objects are often returned from functions
as Use()'d, meaning that the programmer needs to specifically tell
the computer when the resource is no longer needed.
Consider the following example:
/*
* Find an object inside of an avltree.
*/
myobject = FindTreeStringNode(avltree, MY_OBJECT_NAME);
.
.
.
In this example, a resource that was stored under the
"MY_OBJECT_NAME" is looked up in a tree and returned to a program.
Remember that SHADOW is primarily a tool for inter-process sharing
of objects. At any time, some other program might come along,
remove your object from your avltree and try and destroy (free)
that object. To prevent this from happening, the FindTreeNode()
code (on top of which the FindTreeStringNode() code is written)
returns the object it finds "UseObject()'d", so that some other
program cannot pull the object out from under you. It falls to you,
the programmer, to tell the system when you are done using the
returned object.
/*
* Find an object inside of an avltree.
*/
myobject = FindTreeStringNode(avltree, MY_OBJECT_NAME);
.
. <<Read/modify the object>>
.
DropObject(myobject); /* No longer interested in myobject */
After the DropObject() call, the myobject pointer should be
considered invalid, and never used thereafter.
There are several variations on this theme. The examples we
will cover are as follows:
1) Returning objects implies that you must have a a valid
use()d pointer already, and that the function you return
to handles the resouce-tracking DropObject() call properly.
2) Functions declared as returning void * may, in fact, return
objects that require them to be resouce tracked.
3) Some functions "swallow" resources. DropObject() is one
example, but there are others.
Consider the example code above. Imagine if, instead of
performing all reads and modifications on the objects between
the FindTreeStringNode() code and the DropObject() code, we wished
to return the object to a calling function for further processing:
DoSomething()
{
OBJECT myobject;
/*
* First retrieve and preprocess the object in some
* manner, then return to me for further processing.
*/
myobject = getMyLocalObject();
.
. <<Read/modify the object a bit here>>
.
DropObject(myobject); /* No longer interested */
}
.
.
.
OBJECT getMyLocalObject()
{
OBJECT myobj;
/*
* Find an object inside of an avltree.
*/
myobj = FindTreeStringNode(avltree, MY_OBJECT_NAME);
.
. <<Read/modify the object>>
.
return myobj;
}
Notice that getMyLocalObject() does NOT call DropObject() on myobj.
This is because the resource is still needed when it is returned
from getMyLocalObject(). The object is truly no longer needed
only when it is no longer referred to within a sequence of execution.
Thus, it is DoSomething() that actually calls DropObject(), and not
getMyLocalObject().
Now consider the DoShadow() call. It is prototyped as returning
a "void *". Surely many method calls will return other types of data!
Methods may returns strings, numbers, pointers to structures, objects,
etc. It is this last case that we are interested in.
DoSomething()
{
OBJECT myobject;
/*
* First retrieve and preprocess the object in some
* manner, then return to me for further processing.
*/
myobject = DoShadow(controlObject,
NULL,
METH_GET_LOCAL_OBJECT,
METHOD_END);
.
. <<Read/modify the object a bit here>>
.
DropObject(myobject); /* No longer interested */
}
.
.
.
/*
* The method declaration (in part).
*/
ARG_TAG REF_getMyLocalObject[] = {RET_OBJ};
OBJECT getMyLocalObject(METHOD_ARGS)
{
OBJECT myobj;
/*
* Find an object inside of an avltree.
*/
myobj = FindTreeStringNode(avltree, MY_OBJECT_NAME);
.
. <<Read/modify the object>>
.
return myobj;
}
The method declaration almost exactly parallels the function
declaration, except that there is an extra ARG_TAG which
describes what the method expects as arguments and what the method
returns.
This method declaration will work for any kind of method call.
Consider a method invoked synchronously across processes. The
method does not drop one from the usecount of the object and instead
passes the object pointer back to the calling process. The calling
process then calls DropObject(). You can see that the resource is
effectively transferred from one process to another (although that
procedure is not currently recorded anywhere within these processes).
Consider the asynchronous case. While invoking this method
asynchronously is of little to no value, it can happen under
certain error conditions (for instance, if you are invoking a
synchronous inter-process method from a process without an associated
SHADOW process object). In this case, the ARG_TAG informs the
method parsing routines (ParseShadowMessage()) to properly discard
the returned object. The DoShadow() method invocation returns
NULL (as with all asynchronous method invocations). DropObject()
correctly handles the NULL input, however your Read/modify code needs
to handle this case as well!
One more example of the method invocation is included for
completeness. Let's say you had a window class that you were
creating on top of the included ROOT_CLASS. The METH_INIT
method returns a window object which is not only "Use()d" in the
local sequence of execution sense, but is also placed on various
system lists.
OBJECT WinInitMethod(METHOD_ARGS, <window-args>);
{
/*
* Some window initialization.
*/
SetMethodEnd(...); /* Set the METHOD_END in the
function's argument stack
correctly for the superclass'
method. */
return CallSuper();
}
Thus when you call CreateInstance()...:
windowObj = CreateInstance(NULL, WINDOW_CLASS,
META_CLASS,
<window-args>
METHOD_END);
.
.
.
...you will eventually need to both tell the system not to "use"
windowObj locally, AND not to use the windowObj anywhere in the
system lists.
.
.
.
/*
* No longer need windowObj locally.
*/
DropObject(windowObj);
.
.
.
/*
* Cleanup code!
*/
windowObj = getMyLocalWindow();
RemoveObject(windowObj);
RemoveObject() is a linked library call which serves as a
convenient combination of two function calls. First, a
METH_REMOVE is sent to the passed object. This informs the
system to Remove the object from all system lists (for more
details on METH_REMOVE, see below). The windowObj is then
passed to the DropObject() function, which removes the
object from the local sequence of execution. Thus, when
RemoveObject() returns, the windowObj is no longer a valid
window pointer, in exactly the same way that it wouldn't be
valid if passed to DropObject().
RemoveObject() is an example of a function which
"swallows" an object pointer from the local sequence of execution.
DropObject() is an example as well. Another example is the
convenience function SetObject() which transfers ownership of the
passed object from the local sequence of execution to a shared
section of memory and returns the object that used to be at that
memory location back to the program in an atomic fashion.
If, at anytime, you want to retain a local pointer to the object
that you pass into one of these functions, you may use the code as in
the following examples:
RemoveObject(UseObject(windowObj);
-or-
oldObject = SetObject(&memoryLocation, UseObject(newObject));
UseObject() increases the usecount of the pointer (and also returns
the pointer to the object, which allows for the easy, if slightly
confusing, nesting). At some later point you will have to
remember to DropObject() the objects, of course!
The following, slightly less confusing, examples do the same thing:
UseObject(windowObj);
RemoveObject(windowObj);
-or-
windowObj = UseObject(windowObj);
RemoveObject(windowObj);
-or-
newObject = UseObject(newObject);
oldObject = SetObject(&memoryLocation, newObject);
etc.
In addition to the UseObject() and DropObject() calls for
SHADOW's objects, there are analogous functions for string
constants. These string constants, herein referred to as either
system strings or unique strings, will now be discussed. The
pertinent calls to resource tracking are UseString() and DropString(),
which perform reasonably analogously to UseObject() and DropObject().
DUAL-PASS RESOURCE TRACKING
The distinction between DropObject()ing a resource and
RemoveObject()ing is more significant than you might suspect at
first glance. The main problem with using useCounts in any
system is the possibility of a self-referencing object, or
a self-referencing chain of objects. That is, object1
contains a field which points to object1 -- in which case
the useCount will never drop to zero; or object1 contains a
pointer to object2 that contains a pointer to object3 that contains
a pointer back to object1. All three objects' useCounts, in this
case, will never fall to zero.
To avoid this problem, SHADOW objects should use the following
dual-pass algorithm:
1) In the METH_REMOVE method for each object, all references to
other objects should either disappear, or be guaranteed to
never participate in a self-referencing "ring".
2) After a METH_REMOVE method is received by an object, it
must never CREATE or allow the creation of, a
self-referencing "ring" in which it is a participant.
3) When a METH_DESTROY is sent, the object should be destroyed.
Self-referencing may occur, therefore, only between the
METH_INIT call to an object, and the METH_REMOVE call.
This allows self-referencing to exist within your programs, but
defines a procedure for eliminating all of these self-references
without resorting to something time-consuming like garbage
collection. The METH_REMOVE pass is often referred to as the
first pass in the dual-pass resource tracking algorithm, and the
METH_DESTROY is often referred to as the second pass in the dual-
pass resource tracking algorithm. It is up to the individual
programmer to design his or her classes to conform to these
specifications.
Nearly all of the above refers to the GLOBAL resource tracking
facilities of SHADOW. SHADOW also provides a LOCAL resource tracking
facility, which helps to reduce the clean-up code (at the expense of
some init code). When a resource is allocated that you want to be
freed when the process exits, you can pass a pointer to the object
(must be a SHADOW object!) to the AddAutoResource().
AddAutoResource() is another example of a procedure that "swallows"
a resource.
If that resource is later sent a METH_REMOVE by your program
before the program actually exits, you should remove the reference
to it within the program with RemoveAutoResource(). Please see the
AutoDocs for more details. The example code may also be helpful,
although not all of it relies on auto-tracked resources.
When the METH_REMOVE method is sent to your program's process
object, all local resources that were added (via AddAutoResource())
are sent a METH_REMOVE, and are then freed from the process' resource
tree. The RemoveCurrentProgram() sends a METH_REMOVE to
your program's process, for example.
SYSTEM STRING CONSTANTS
SHADOW uses the concept of unique strings as borrowed from the
NeXT platform. Each string is assigned a unique address by the
system. Two or more strings of the same contents have the same
system address. This allows the system to find strings faster --
by comparing the address, rather than the contents.
The implementation is via the UseString/DropString/FindString
library functions, as well as the QuickUseString and QuickDropString
functions. System strings are stored on a 1024 entry hash table
where collisions are linked by a sorted binary tree. This table is
found in ShadowBase->sb_systemStrings.
UseString() will lookup the passed string in the table. If it
does not find it, the string is created and stored into the table.
Otherwise, the object's usecount of the located system string is
incremented. The system string's address is then returned.
DropString() will lookup the passed string in the table, and,
if found, the string's useCount is decremented. If the usecount has
dropped to zero, the string object is freed and removed from the
system string list.
FindString() returns the address of a requested system string,
if one can be found. It will not increment the usecount, and
therefore should only be used as an address, rather than as an
actual string pointer, as that string may go away unexpectedly
at any time.
QuickUseString() and QuickDropString() take known system string
addresses and manually increments or decrements the usecount. If
the usecount drops to zero, the normal DropString() is used to
remove the string from the system. Be sure that you actually
have a system string pointer before claling these functions! These
functions exist for performance reasons only. It is preferrable
to go through the UseString() and DropString() calls instead.
Consider the following:
struct StringData {
char *sd_name;
ULONG sd_data;
};
struct StringData MyArray[] = {UseString(-blah-), 1,
UseString(-blah-), 2,
etc.};
We can now qsort the array on the sd_name field and lookups can be
as fast as calling a radix search function!
/*
* Look up item in qsort()'d MyArray
*
* [You can find a decent Radix_Search function in nearly
* every computer algorithms textbook.]
*/
Radix_Search(&MyArray, 0,
sizeof(MyArray)/sizeof(struct StringData),
UseString(look_for_this_string));
DropString(look_for_this_string);
.
.
.
/*
* free all the strings.
* Clean up!
*/
for(i = 0; i < sizeof(MyArray)/sizeof(struct StringData), i++)
DropString(MyArray[i].sd_name);
Not only does this improve access by making an O(n) process into an
O(log n) one, but the entire string compare overhead is eliminated
by calling UseString() once (instead of strcmp() 'n' times)!
System strings are used extensively by the object-oriented subsystem
for such things as lookups of methods and attributes, and for passing
strings ('JSTR's) between processes during asynchronous method calls.
They are also used by the AVL tree subsystem which will be covered
just as soon as we treat the semaphore subsystem.
SEMAPHORES
This discussion of SHADOW's semaphores closely follows the
RKM:Libraries Third Edition's discussion of Exec's semaphores. It is
assumed in this section that you have either read this chapter or
learned about Exec semaphores from some other source, and that you
understand the principle concepts behind semaphores.
Understanding semaphores is a prerequisite to understanding a lot of
the deeper issues within SHADOW. This is why I treat it very close
to the beginning of this introduction. If you do not understand
semaphores, it is highly recommended that you either buy the
RKM:Libraries book, or some general purpose Operating Systems
textbook.
Like Exec's semaphoring system, SHADOW's semaphoring system contains
the basic Shared and Exclusive access constructs. Under Exec, the
following calls provide these access constructs:
AttemptSemaphore(<semaphore>)
ObtainSemaphore(<semaphore>)
ObtainSemaphoreShared(<semaphore>)
ReleaseSemaphore(<semaphore>)
The analogous calls in SHADOW are as follows:
PSem(<address>, SSEM_ATTEMPT | SSEM_LOCK);
PSem(<address>, SSEM_LOCK);
PSem(<address>, SSEM_READ);
VSem(<address>);
Using the macros defined in <shadow/semaphore.h> these become:
PSem(<address>, SSEM_ATTEMPT | SSEM_LOCK);
RWLock(<address>);
ReadLock(<address>);
VSem(<address>);
However, unlike Exec's semaphoring system, SHADOW does not have
calls that are directly analogous to the AddSemaphore(),
RemSemaphore(), FindSemaphore() and InitSemaphore() library calls of
Exec. Instead, SHADOW allows you to semaphore any arbitrary address
(excepting (void *)-1 which is reserved). The contents at this
address do not participate in the semaphoring process. This implies
that there are no SHADOW semaphore structures analogous to Exec's
SignalSemaphores. Instead, all legal addresses act as global
semaphore points. SHADOW takes care of creating, initializing, and
destroying all structures needed to internally manage the semaphoring
process, and because the Amiga's address space is linear across all
processes, all semaphores are global by nature. This eliminates the
need for the AddSemaphore(), the RemSemaphore(), the FindSemaphore(),
and the InitSemaphore() calls, thus simplifying the programmer's
task.
Consider two threads running through the following program, one at
Reader(), one at Writer().
UBYTE *ASharedString;
Reader()
{
while(TRUE)
{
ReadLock(ASharedString);
printf("The string %s\n", ASharedString);
VSem(ASharedString);
}
}
Writer()
{
while(TRUE)
{
WriteLock(ASharedString);
gets(ASharedString);
VSem(ASharedString);
}
}
"ASharedString" acts as a shared string between the Reader() and
Writer() threads. To coordinate access to the string, the
address of the string is also used as the semaphoring address
point. while this is the most straightforward way of managing the
semaphore system, there is nothing wrong with creating an arbitrary
semaphoring address point. For example, you might arbitrate access
for the string by semaphoring against the Reader()'s function address!
UBYTE *ASharedString;
Reader()
{
while(TRUE)
{
ReadLock((void *)Reader);
printf("The string %s\n", ASharedString);
VSem((void *)Reader);
}
}
Writer()
{
while(TRUE)
{
WriteLock((void *)Reader);
gets(ASharedString);
VSem((void *)Reader);
}
}
One warning, however. You are not allowed to semaphore on an address
that you do not in some way possess. This is to guard against the
possibility that some other process gets started that actually -does-
use this address. Because SHADOW's semaphoring points are
automatically public, when this other process attempts to semaphore
against its address it will come into conflict with your programs.
This is potentially deadly.
As a corollary, you must release all semaphores that you obtained
before your program exits and/or before the address that the
semaphore is arbitrating is freed back to Exec's memory pools.
To aid in testing, you are directed to the sb_semTree field
within ShadowBase which acts as the base of all semaphore allocations
that SHADOW makes. This field is zero when there are no outstanding
semaphores.
In addition to not having the complications of semaphore
structures and the (unnecessary, on the Amiga) distinction between
global and local semaphores, SHADOW does not offer Exec's prepackaged
ObtainSemaphoreList() and ReleaseSemaphoreList(). While these calls
are sometimes important to use to avoid deadlock situations, it
is entirely possible to build your own SemaphoreList wrappers by
clever use of the SSEM_ATTEMPT flag. This is left as the proverbial
exercise to the reader.
Instead, SHADOW expands on Exec's usage of semaphores to
include intra-process locking integrity, counting semaphores,
conditional variables, and multi-process signalling. We take each
one in turn, and then address performance issues.
Intra-Process Locking Integrity
This may be a confusing amalgamation, but the concept is simple.
Take an example of a library function IterateList(). The purpose
of IterateList() is to call a function on each of the members on
a shared list. We might implement this as follows:
IterateList(struct MyList *list, void (*func)(struct MyNode *node))
{
struct MyNode *node;
PSem(list, SSEM_READ);
node = list->first;
while(node)
{
func(node);
node = node->next;
}
VSem(list);
}
Consider, however, what might happen if we pass a function which
removed the node from the list it was in. Clearly this would have
very bad consequences for IterateList()!
Therefore, instead of the normal Exclusive/Shared access locks,
SHADOW provides three types of access locks -- an SSEM_READ,
an SSEM_WRITE, and an SSEM_LOCK (a read-write lock). SSEM_READ
locks are shareable across processes and are nestable within
processes. SSEM_WRITE is neither shareable nor nestable. SSEM_LOCK
is not shareable, but is nestable -- with the following modifications:
a) Multiple SSEM_LOCK requests always nest.
b) If you request an SSEM_READ on an SSEM_LOCKed semaphore,
the semaphore becomes a nestable, non-shareable,
SSEM_READ semaphore. The semaphore reverts to its earlier
Read-Write status upon execution of the matching VSem() call.
c) If you request an SSEM_WRITE on an SSEM_LOCKed semaphore,
the semaphore becomes a non-nestable, non-shareable,
SSEM_WRITE semaphore. Again, the semaphore reverts
to its earlier Read-Write status upon the matching
VSem() call.
It is easier to understand this nesting ability by focusing on the
Read/Write characteristics of these different locks, rather than
on their Shared/Nestable characteristics.
Any number of processes can Read. Only one process may Write, and
no one may be Reading at the time. The Read-Write is used for
mutual exclusion across processes and can become either a Read-only
or a Write-only semaphore as needed -- although the semaphore will
always remain non-shareable until the last Read-Write semaphore
access has been released via VSem().
Consider the following example taken from the RemoveResources()
function in SHADOW:
.
.
.
PSem(&tree, SSEM_LOCK);
DoInOrderTree(&tree, RemoveCompInstance, NULL);
FreeTreeAllNodes(&tree);
VSem(&tree);
.
.
.
Here we are attempting to eliminate each node from a tree and invoke
the METH_REMOVE method on each node (object) on the tree. Because of
the organization of the AVLTree functions (described below), this is
a two stage process -- we invoke the METH_REMOVEs on all objects in
the tree (DoInOrderTree()), and then remove all objects from the
tree (FreeTreeAllNodes()).
As in the above list example, the DoInOrderTree() library function
locks the tree with an SSEM_READ semaphore call. The
FreeTreeAllNodes(), on the other hand, locks the tree with an
SSEM_WRITE semaphore.
In order to be truly safe, we need to ensure that no other process
adds a node between the DoInOrderTree() call and the
FreeTreeAllNodes() call where the tree would normally be accessible.
To accomplish this, we get an SSEM_LOCK semaphore to surround the two
function calls. The SSEM_LOCK provides the nesting that is needed by
the two library functions which acquire their own semaphores, while
maintaining the exclusive access characteristics required by the code.
In addition, because SSEM_READ/SSEM_LOCK nesting is different
from SSEM_LOCK/SSEM_LOCK nesting, the intra-process locking integrity
is maintained. Any attempt to gain write or read-write access to the
tree from within the DoInOrderTree() call will halt the program, and
any attempt to lock the tree at all within the FreeTreeAllNodes()
will halt the program. This allows the programmer to debug the
problem when it occurs, instead of trying to deduce what occurred
from problems which might have normally ensued.
Compare that to the alternative as suggested by V39Beta Exec
semaphores. Not only does Exec fail to make a distinction
between Write and Read-Write semaphores, but when you request
a shared semaphore on a semaphore that you already have locked
exclusively, you get another exclusive access granted! This
precludes the potential debugging advantages of SHADOW's semaphores.
[Of course, it's even worse when you consider that V37 semaphores
did something entirely different under this exact same set of
circumstances anyway....]
Counting Semaphores
Counting Semaphores are a very straightforward addition to most
semaphoring systems. A typical example is with resource
allocations. Consider the problem of an audio.device type interface
where you have four available channels. All four channels are
allocated, and a program is waiting for any two.
Here is where counting semaphores come in. You create a
counting semaphore by using the CreateSemaphore() macro, passing
in the maximum number of available resources. It returns the
semaphoring address point that you can use in the rest of
your calls. You should later free this count semaphore by
calling DestroySemaphore(), passing in the same address as
was returned by CreateSemaphore. The ObtainNumber() and
ReleaseNumber() macros can be used on this address between
the Create() and Destroy() calls to obtain and release a
certain number of resources.
APTR GlobalResourceSemaphore = CreateSemaphore(4);
.
.
.
/* Obtain two channels. */
ObtainCount(GlobalResourceSemaphore, 2);
.
.
.
/* release a channel back to the system */
ReleaseCount(GlobalResourceSemaphore, 1);
.
.
.
/* Obtain three channels */
ObtainCount(GlobalResourceSemaphore, 3);
.
.
.
/* Prepare to destroy the semaphore */
DestroySemaphore(GlobalResourceSemaphore);
/*
* All resources have been freed, semaphore is now freed
* as well.
*/
ReleaseSemaphore(GlobalResourceSemaphore, 4);
Notice carefully that you can ask for any number of resources --
if you ask for too many, you may hang indefinitely, and as there
is no priority to your semaphore requests, starvation is definitely
possible.
Also note that the semaphore is not actually freed from the system
until all processes release their locks and no process is
waiting on the semaphore, irregardless of the call to
DestroySemaphore().
In addition to these calls, you can call the UpCountSem() and
DownCountSem() macros which create the semaphore automatically
from the passed address. This eliminates the need to call the
CreateSemaphore() and DestroySemaphore() macros, but it limits you
to single-step increment/decrement calls.
ULONG SemaphorePoint;
/* Obtain two channels of four. */
UpCount(&SemaphorePoint, 4);
UpCount(&SemaphorePoint, 4);
.
.
.
/* release a channel back to the system */
DownCount(&SemaphorePoint);
.
.
.
/* Obtain three channels of four*/
UpCount(&SemaphorePoint, 4);
UpCount(&SemaphorePoint, 4);
UpCount(&SemaphorePoint, 4);
DownCount(&SemaphorePoint);
DownCount(&SemaphorePoint);
DownCount(&SemaphorePoint);
DownCount(&SemaphorePoint);
/*
* All resources have been freed, semaphore is now freed
* as well (automatically).
*/
Conditional Variables
Conditional Variables keep track of the number of "conditions" that
have occurred. A task can cause any number of conditions, and can
wait for any number of conditions.
Consider the Reader() and Writer() problem above. Consider what is
really happening. It is quite possible for a single string to be
printed out between zero and -thousands- of times. In reality, what
we want is to have each string printed out once. Here, the Reader()
and Writer() example becomes the more classic Consumer() and
Producer() problem. Here is an example implementation using
conditional variables:
extern void Producer(void);
UBYTE *ASharedString;
void main(void)
{
SetCondition(((char *)Producer) + 1, 1)
/*
* Default to produce the first string.
* We can obviously default to consuming the
* first string as well....
*/
startpThreads();
}
void Consumer(void)
{
while(TRUE)
{
WaitCondition(((char *)Consumer) + 1, 1);
/*
* Wait for a new string.
*/
printf("The string %s\n", ASharedString);
/*
* Print the new string
*/
SetCondition(((char *)Producer) + 1, 1);
/*
* The string has been consumed, want another one!
*/
}
}
void Producer(void)
{
while(TRUE)
{
WaitCondition(((char *)Producer) + 1, 1);
/*
* Wait for a need for a new string.
*/
gets(ASharedString);
/*
* Get another string.
*/
SetCondition(((char *)Producer) + 1, 1);
/*
* The string has been produced!
*/
}
}
This code will work with as many Producer and Consumer threads as
you like. Only one condition occurs, so only WaitCondition()
is satisfied for each SetCondition() call. Also, the startup
case assumes that no string is, at first, available. The code
would be slightly different without that assumption.
Also notice that these programs use -odd- addresses for condition
variables. This is to avoid potential address collisions. Most
lock functions occur at even addresses, so I have all of my
programs use the odd addresses for conditional varialbes, thereby
eliminating a potential source of confusing. There is NO dictum
that requires you to follow in my footsteps, though.
Multi-Process Signalling
Multi-Process Signalling allows an arbitrarily large number of
programs to find out when something happens. Where condition
variables keep track of the number of conditions that occurred
but haven't been handled, the Multi-Process signalling
cannot keep track of the number of "outstanding" conditions
that have occurred. However, it can wake up EVERYONE
waiting on the WaitLevel() macro.
WaitLevel(address); waits for a condition to occur associated
with the passed address.
SetLevel(address); makes the multi-process signalling
condition occur.
Examples are left as an exercise for the reader.
Performance Issues
Considering the extended number of features that SHADOW
includes, and especially considering that the vast majority of
semaphores are created and deleted on the fly, it may not be too
surprising to discover that SHADOW's semaphoring system is slower
than Exec's. Indeed, it would be miraculous if it weren't. What
is surprising is that SHADOW's semaphores are very competitive with
Exec's, and if we reduce every exec semaphore access to include a
FindSemaphore() call (a Find() on a string that is less than four
byteslong, even), we find that SHADOW's semaphores run at about
the same speed as Exec's semaphores.
To be more precise, SHADOW's semaphores have a performance
overhead of about 2-5x that of Exec's local semaphoring system (that
is, Exec's semaphores without the overhead of FindSemaphore()) and
about the same overhead as Exec's global semaphoring system (that is,
Exec's semaphores with the FindSemaphore() overhead).
EXEC's global semaphoring system has a frightening overhead when
either large strings (more than four characters) or large numbers of
semaphores are used. For instance, if each string is about sixteen
characters long instead of four, Exec does only about half as well as
SHADOW. Because SHADOW uses addresses as a semaphoring point,
instead of strings, and because SHADOW stores its semaphores on a
binary tree, instead of a linked list, SHADOW's overhead grows
slower than Exec's. The test cases sited here are performed in the
included "perftest" program, and uses only 12 semaphores at a time --
a reasonable figure, it seems to me.
But what if you want to use SHADOW's semaphores and key the
semaphores on a string? Use the PSemString() and VSemString() calls!
Essentially, the string is UseString()d by the system, and the
address of the returned string is used as the semaphoring point.
This allows for both the flexibility of named semaphores, while
allowing for the speed of unnamed semaphores.
A good use for named semaphores is for coordinating the startup
of a program. Consider what happens if two copies of the same program
get launched nearly simultaneously. Because each program defines
the same classes to the system (usually), it is wise to start each
program only once, and then merely instantiate another existence
of the program once started. This provides the flexibility of
"resident" programs, without the constant overhead for programs
that aren't needed.
Unfortunately, it is difficult to coordinate the simultaneous
startup of programs. Browser uses the following code:
PSemString("Browser Program", SSEM_LOCK);
/*
* CreateInstance() only works if the BLOCK_CLASS is already
* defined (which means that another browser program has been
* launched).
*
* Otherwise, we're the first browser program, and will have to
* define BLOCK_CLASS, etc.
*/
if (CreateInstance(NULL, BLOCK_CLASS, META_CLASS, METHOD_END))
{
VSemString("Browser Program");
return;
}
.
. /* define BLOCK_CLASS, etc. */
.
VSemString("Browser Program");
.
.
.
Here we see that if two Browser Programs startup, the code is
guaranteed to define the BLOCK_CLASS in one program, and then
instantiate an instance of BLOCK_CLASS in the other.
--
Getting back to performance issues, another advantage to SHADOW's
semaphores is that they take less space than their Exec counterparts,
first because they don't require the overhead of semaphore-lists, and
second because the semaphores don't need to exist in memory until
needed. It is worth noting that SHADOW will pseudo-busy wait if it
tries to allocate a semaphore structure but runs out of memory,
unless, of course, it was passed the SSEM_ATTEMPT flag -- in which
case it fails.
So we conclude that while Exec semaphores have some speed
advantage in the simple cases, they are slower when sharing semaphores
across more than one program, always take more memory, and are more
difficult to setup and use. I definitely recommend the SHADOW
subsystem over the Exec subsystem, if only for the flexibility that
it offers unless speed is absolutely crucial.
AVLTREES
AVL trees are auto-balancing binary trees. Again, this
discussion assumes that you have read a decent algorithms book which
would cover AVL trees. I suggest "Data Structgures and Program
Design in C" by Robert L. Kruse, Bruse P. Leung, and Clovis L. Tondo.
SHADOW's AVL trees are built on top of the semaphore code, the
memory code (discussed below), the resource tracking code, and, in
some part, the system string code. They are designed for quick
access by many processes and are used to store metas, classes,
and other instances within SHADOW. Again, the design goal of SHADOW
is to provide for multi-threaded computing, so all AVL trees are
semaphored when accessed by the system.
To initialize an avl tree:
AVLTree bt = NULL;
Now you can add any number of SHADOW-objects (the definition of
exactly what a SHADOW-object is is located below) to the binary tree.
Indeed, you may add any number of objects with any key to any number
of binary trees. AVLTrees usually require you to have different
keys for each inserted item. SHADOW does not impose this
restriction. Exec requires you to have one Node structure for
each list that an "object" might belong to. Again, SHADOW does
not require this. SHADOW allocates and frees its own BinNodes
internally when required. In addition, to support the idea of
non-unique keys, the RemoveTreeNode() function takes the key AND
the address of the object you want removed -- if you don't know the
address of the object, you can find it by calling FindTreeNode()
with the key. The first object found with that key will be returned,
and you may pass this to the RemoveTreeNode() function.
OBJECT object1 = <>, object2 = <>;
AddTreeNode(&bt, object1, 12);
AddTreeNode(&bt, object2, 13);
/*
* Note: object1 and object2 have not been swallowed,
* so swallow them now!
*/
DropObject(object1);
DropObject(object2);
.
.
.
obj = FindTreeNode(&bt, 13);
/*
* do something useful with obj!
*/
DropObject(obj);
.
.
.
/*
* Now we remove all of our nodes from the tree.
*/
obj = FindTreeNode(&bt, 12);
RemoveTreeNode(&bt, obj, 12);
DropObject(obj);
obj = FindTreeNode(&bt, 13);
RemoveTreeNode(&bt, obj, 13);
DropObject(obj);
/*
* Alternatively, we can call FreeTreeAllNodes()
* providing we meet the following specifications:
*
* All nodes that were added must have been added to
* the tree via the Add*TreeStringNode() functions.
*
* All nodes not added this way must have keys
* between 0 and 255 or keys equal to the object's
* address [ie: AddTreeNode(&bt, obj, (ULONG)obj)].
*
* FreeTreeAllNodes(&bt);
*
* This removes all nodes from the tree and drops all
* the applicable resources.
*/
SHADOW provides functions which sort the objects by a string. Do
not fool yourself into believing that this produces an object tree
which is sorted alphabetically -- SHADOW's string routines are built
around the system strings mentioned above. Sorting of the nodes
occurs according to the -address- of the system string, NOT the
contents of the string itself.
/*
* Add some objects keyed on some strings.
*/
AddTreeStringNode(&bt, object1, "Sort me!");
AddTreeStringNode(&bt, object2, "Me too!");
DropObject(object1);
DropObject(object2);
.
.
.
/*
* We now demonstrate both lookups of objects using
* a string as a key, and removal of objects from
* trees where the object was keyed on a string.
*/
object1 = FindTreeStringNode(&bt, "Sort me!");
object2 = FindTreeStringNode(&bt, "Me too!");
RemoveTreeStringNode(&bt, object1, "Sort me!");
RemoveTreeStringNode(&bt, object2, "Me too!");
DropObject(object1);
DropObject(object2);
SHADOW also provides a routine which will call a function on each
node within the bintree -- passing in the node, the key, and a
value passed into the RecurseTree() function.
/*
* Example RecurseTree() callback.
*
* Here we are looking for something inside an object
* that matches the passed "passedIn" variable.
*
* We will return the object if found, or NULL to
* continue looking.
* The Recurse() function then returns what we
* return, or NULL if we never return anything.
*/
void __regargs *myFunc(OBJECT obj, ULONG key,
void *passedIn)
{
struct SomePrivStruct *sps;
sps = FindAttribute(obj, ATTR_PRIVATESTUFF);
if (sps->sps_searchMe == passedIn)
return UseObject(obj); /* return object used! */
return NULL;
}
.
.
.
/*
* Find the object that has the correct info inside it.
* Search all objects on the binary tree -- we don't
* particularly care about the order.
*
* The number 67 is passed to the myFunc callback as
* the "passedIn" variable of that function.
*/
found = RecurseTree(&bt, myFunc, (void *)67,
SHADOW_RECURSE_INORDER);
.
.
.
DropObject(found);
Here we have demonstrated a search implemented via the RecurseTree().
Using this function we can also duplicate an entire tree:
/*
* We assume that the binary tree had all of its objects
* stored according to a system string object.
* Adjustments would have to be made if this were not so.
*/
void __regargs *dupStringFunc(OBJECT obj, ULONG key,
AVLTree *passedIn)
{
AddTreeStringNode(passedIn, obj, (char *)key);
/*
* Remember to return NULL to continue the
* recursion!
*/
return NULL;
}
.
.
.
/*
* Duplicate the avl tree.
* bt gets duplicated onto bt2. Don't forget to set bt2
* to NULL to start with!
*/
bt2 = NULL;
RecurseTree(&bt, myFunc, &bt2, SHADOW_RECURSE_INORDER);
/*
* bt2 now has a direct copy of bt.
*/
.
.
.
/*
* Cleanup!
*/
FreeTreeAllNodes(&bt);
FreeTreeAllNodes(&bt2);
There are several different ways to recurse down the binary tree:
SHADOW_RECURSE_PREORDER
SHADOW_RECURSE_INORDER
SHADOW_RECURSE_POSTORDER
SHADOW_RECURSE_BACKORDER
PREORDER visits the nodes as Root, left-tree, right-tree. INORDER
visits the nodes as left-tree, Root, right-tree -- giving an
increasing key traversal. POSTORDER visits the nodes as left-tree,
Root, right-tree. BACKORDER visits the nodes as right-tree, Root,
left-tree -- giving a decreasng key traversal.
Several macro functions have been provided in much the same way that
macros are available for many of the different PSem() semaphore calls.
These are:
DoPreOrderTree(&bt, func, passedIn)
DoInOrderTree(&bt, func, passedIn)
DoPostOrderTree(&bt, func, passedIn)
DoBackOrderTree(&bt, func, passedIn)
They directly correspond to the flags of similar name.
There are several important things to remember when using SHADOW's
AVL trees. For one thing, some of the functions assume that the
nodes are sorted by system string address (FreeTreeAllNodes()),
and won't be happy if they find out otherwise. Other functions,
like RecurseTree() don't care, but the callback functions supplied
to this call might.
In addition, it is important to remember that all nodes are sorted
on unsigned keys, not signed keys. Also, all objects added to avl
trees must be SHADOW objects, not some concoction of your own.
This either means that the first three longwords correspond to the
struct ClasslessObject definition, or that the object was
created via a CreateInstance()/CreateSubClass() or equivalent call.
Performance Issues
SHADOW's AVL tree functions (apart from the *Watched*() functions
which are really a part of another subsystem) are implemented in
assembly. They are guaranteed to use less than 256 bytes of stack,
independent of how large the tree grows -- assuming the tree resides
in a 32bit address space, that is.
Despite the relatively large overhead implied by the use of SHADOW's
semaphores, a node can be found in a 256 node tree in an average of
84 microseconds on my A3000 -- an object stored with a string as
the key would take slightly longer, depending on the length of the
string.
Adding and removing nodes takes slightly longer. Again, with an
average of 256 nodes inserted and removed in order, the total
add-remove cycle takes about 280 microseconds (on the A3000).
This is not bad, considering that the binary nodes used internally
by the AVLTree subsystem are allocated and freed as needed. Exec
lists, while they do not have this assorted cost, are less flexible
about how many times an "object" can be added and to how many trees.
You should pick the storage structure that is best suited for you.
Fortunately, SHADOW now gives you a choice, without forcing you to
do your own implementation.
Summary
In summary, there are only four really important AVLTREE calls to
understand:
AddTreeNode() adds an object to an AVL tree sorted by a
passed key.
RemoveTreeNode() removes an object from the AVL tree.
FindTreeNode() finds the object in an AVL tree, and returns
this object. You should DropObject() that
object when you are done using it, as is TRUE
of any resource that SHADOW returns to you.
RecurseTree() calls a function for every object in the binary
tree.
The other function which is NOT a derivative of these four is
FreeTreeAllNodes(). It's a pretty esoteric call -- it just removes
all objects from the AVL tree, though. Nothing special. [Note
the restrictions on its use!]
All other AVL tree calls are derivatives of the first three calls,
and four other calls [DoPreOrderTree() for example] are built
directly on top of RecurseTree() as #defines macros.
SLISTS
SLISTs are single-linked, prioritized lists. The salient
functions are:
AddSListNode()
RemoveSListNode()
FindNodePriInSList()
Like their SHADOW avltree cousins, these functions are semaphored and
the objects added into the lists are resource-tracked -- so all
objects added to these lists MUST be SHADOW-objects.
The only advantages they add over Exec is the resource tracking, the
automatic semaphores and the ability for objects to reside in
multiple lists, multiple numbers of times. The real disadvantage is
that there is a great deal of overhead for adding prioritized
nodes, and removing nodes.
Use them if you find them useful.
MEMORY
In this discussion about semaphores and AVLTrees and SLists, we
have glossed over one consistent point -- memory management. For
instance, we know that SHADOW's semaphores have some kind of
internally managed semaphore object, we know that AVLTrees have some
kind of internally managed BinNode structures, and we know that
SLISTs have some kind of internally managed Node structures, but we
don't know how these structures are allocated and freed.
SHADOW has a memory subsystem that is responsible for dealing with
these tasks. While its main purpose is to create and destroy these
small structures as fast as possible, it has enough hooks to be
useful to anyone who has to manage a large number of structures of
exactly the same size.
The following are the functions which implement SHADOW's MEMORY
subsystem:
result = (BOOL)InitTable(SEMLIST list, allocfunc, freefunc, size)
memory = (void *)AllocateItem(SEMLIST list)
(void)FreeItem(SEMLIST list, (void *)memory)
FreeTable(SEMLIST list)
You use these memory tables by first calling the InitTable(),
using the AllocateItem() and FreeItem() calls, and then calling
FreeTable().
struct MemoryList globalMemList;
InitTable(&globalMemList, NULL, NULL, 16);
This initializes the memory table to use the default allocation and
free calls (AllocMem(MEMF_PUBLIC)/FreeMem()). In addition, it sets
up the memory table to dole out memory in 16byte chunks, and
pre-allocates 32 items.
Each item allocated from the table will be 16bytes long. The contents
of the items are not guaranteed to be initialized to zero, EVEN if you
use your own function callbacks to allocate the memory as MEMF_CLEAR.
Items are allocated from Exec's memory pools 32chunks at a time.
When enough items are put back into the table, the table will free
a full 32chunks back into Exec's free memory pool. The definition
of "enough" is purposely left vague.
memory = AllocateItem(&globalMemList);
FreeItem(&globalMemList, memory);
When all allocations are finished and you are done using the memory
list, call FreeTable(). This frees all the still allocated items
that you might have left allocated, and it returns all resources
to Exec's free memory pool.
memory = AllocateItem(&globalMemList);
.
.
.
FreeTable(&globalMemList); /* All outstanding allocations are
returned to Exec */
Here are the default allocmem() and freemem() callbacks that SHADOW
uses to allocate and free memory from Exec. These functions are
called everytime a new chunk of 32items is needed from Exec.
void * __regargs temporaryAlloc(struct MemoryList *list)
{
return AllocMem(32 * list->memlst_size +
sizeof(struct MemoryNode),
MEMF_PUBLIC);
}
void __regargs temporaryFree(struct MemoryList *list,
struct MemoryNode *node)
{
FreeMem(node, 32 * list->memlst_size +
sizeof(struct MemoryNode));
}
Again, because SHADOW is principly designed for multi-threaded work,
these memory lists use semaphores for each AllocateItem() and
FreeItem() call. If you don't need this protection, set the
memlst_semUse to FALSE, and the EXEC Signal Semaphore that is a part
of the MemoryList structure will not be used. [You should set
the semUse flag AFTER the InitTable() is called, and not before!]
The relative performance of SHADOW is hard to measure because Exec's
memory allocation and freeing routines can differ by a factor of
five. However, SHADOW's memory allocation routines, even
WITH the semaphore, are about as fast as I've ever seen Exec's
routines. Without the semaphore they are obviously faster.
What is plainly (painfully!) obvious is how bad Exec's routines
degenerate when frees are perfectly interleaved (that is, when
all of the odd numbered allocations are freed first, and then
all the even numbered allocations are freed). SHADOW is
faster in this case by, at least, a factor of ten!
Obviously, if the semaphore is not needed, SHADOW's memory routines
are much faster than Exec's memory routines. No attempt has been
made to compare SHADOW's routines with anything other than Exec's
AllocMem() and FreeMem() calls. It is possible that some of Exec's
other routines would be faster (in certain cases) than SHADOW's.
It is up to the programmer to decide which set of functions best
suits his or her purpose.
OOPL
Now that you know a little about the functional interface to the
library, it is high time to introduce the object-oriented interface.
However, this seems to be a good time to mention what SHADOW is NOT.
SHADOW is not an Object-Oriented Programming Language (OOPL). It is
also not a Concurrent-Object-Oriented Programming Language. Indeed,
it is not a language at all. It is entirely possible to envision a
language built around SHADOW at some point in the future. For right
now, however, SHADOW is accessed via the 'C' language.
So why not have built around an existing object-oriented language?
True, the addition of semaphores, avl-trees, system strings, and
even (with a bit of work) resource tracking, would all be possible
under a language such as C++. However, rewriting C++'s method
dispatcher to provide for the synchronous and asynchronous method
sends would have proved difficult, as C++ doesn't have one.
Indeed, the languages that do exist which have concurrency syntax
are usually built not only as "concurrent" languages, but as
"distributed, concurrent" languages. This type of abstraction
is not necessary on the Amiga as the Amiga has only a single
"process-space". And yes, this implies you can't magically
hook up two Amigas with SHADOW and expect things to work
twice as fast together (or together at all at whatever speed).
Remember, the design goal of SHADOW was to take advantage of
the Amiga's better facilities -- principally among them, low
overhead task switching and shared memory, not to add
functionality.
Object Oriented
So what is object-oriented programming anyway? The classical
definition of object-oriented programming is by example --
ie: object-oriented programming was introduced to the world as a
brand new way of programming, but the only formal definition
offered was the example language implementations at the time
(Smalltalk being the better known of these). This is an unacceptable
definition. Unfortunately, the term has grown to mean a number
of things to a number of people.
However, there are some general "philosophies" behind Object-Oriented
Programming. Object-Oriented Programming attempts to model the
world as a collection of "things" where these "things" have
certain "characteristics" and are capable of performing certain
"actions". For instance, dogs are things that have certain hair-
color, height, weight, etc. Dogs can also do things like "bark",
"eat", "sleep", etc.
In addition to this, many object-oriented languages draw the
distinction between definitive "things" and indefinite "things".
For example, "-a- dog" is an example of an indefinite thing,
whereas "-the- dog Rover" is an example of a definitive thing.
As SHADOW makes this distinction this is not of purely
academic interest.
Object-Oriented Programming offers some names for the above, however
while many of the names are consistent, some of them are not. We
will use the terminology that SHADOW uses. if you wish to
understand SHADOW against the wider diversity of object-oriented
systems, you'll have to do some more reading.
Under SHADOW, "things" are referred to as objects. Often you will
see the term in uppercase as in "OBJECTs" -- this is because
"OBJECT" is an actual 'C' defined type (typedef), there is no
difference here. Some later distinction is drawn between "Object"
and "object," but that is immaterial to this discussion. Under
SHADOW, indefinite things are objects, definite things are objects,
everything is an object.
An indefinite object is referred to as a class (or, alternatively in
some of the docs, as a "cob_class"). A definite object is called
an instance. A definite object is always an instance of a particular
corresponding indefinite object. For example, "Rover" is an
instance of "dog":
Real World Object World Description
---------- ------------ -----------------
DOG -- class -- indefinite object
Rover -- instance -- definite object
In addition, these object have physical "characteristics" and they
possess the ability to perform "actions". Under SHADOW, these
charateristics are referred to as attributes, and the actions are
referred to as methods. In addition, attributes and methods are
both -described- in a class, not in an instance. For example,
when we model a dog in the computer system, we define certain
characteristics and actions that dogs, in general, will have
and perform. A dog has a certain hair color, weight, height,
and they all know how to bark, eat, and sleep. Rover, as an
instance of this imaginary dog-class, automatically would contain
the space for its characteristics, and could call the "bark",
"eat", and "sleep" routines that were specified in the class.
We can imagine some simple code that illustrates this:
new CLASS called DOG {
Attributes:
weight is a float
height is a float
hair-color is a set containing (black, brown, white)
Methods:
bark:
play_sound "dh0:samples/barkingDog"
end
eat:
say "Munch, munch, slurp, slurp, munch, munch."
end
sleep:
display "dh0:pictures/SleepingDog.pic"
end
}
new INSTANCE called ROVER of CLASS DOG {
weight = 13.2
height = 33.6
hair-color is (brown, black)
}
At this point you can do something like:
ROVER bark
and ROVER would 'play_sound "dh0:samples/barkingDog"'.
Obviously, while the syntax is different, there hasn't been much
gained over straight 'C' here. But we aren't quite finished yet!
There is yet one more feature to Object-Oriented Programming, and
that is the notion of "subclassing". Subclassing is the heart
of "reuse' -- the notion that old code never dies, it just gets
recycled. Let's take the same dog example again. Dogs are nice,
for some people, but others prefer a particular -subclass- of
dogs, say, hunting dogs. A hunting dogs do useful things like
retrieving dead ducks from slimey swamp pits. So, we have the
following pseudocode:
new CLASS called HUNTING_DOG as SUBCLASS of DOG {
methods:
retrieve:
exists duck?
print "Dog now has duck"
else
object bark
end
}
new INSTANCE called SPOT of class HUNTING_DOG {
weight = 15.2
height = 30,5
hair-color is (black, white)
}
Notice that the HUNTING_DOG instance SPOT can specify all the
attributes that ROVER did. This is because HUNTING_DOG "inherits"
the attribute definitions from its "superclass", DOG.
In addition, if there was no duck, and you did the following:
SPOT retrieve
SPOT would bark (else... object bark)! Just as HUNTING_DOG
inherits attributes, it inherits the methods of DOG as well.
There is one more possibility:
ROVER retrieve
Remember, ROVER is not a HUNTING_DOG. ROVER, therefore, does not
know how to retrieve Under SHADOW, nothing happens (an error
code is set in your process object, though). In C++ this causes
a compile-time error, and in Objective-C it causes a program
failure ("Unimplemented Method"). SHADOW is a bit more forgiving
than either of these languages....
Object-Orientedness under SHADOW
That taken care of, you can imagine that creating class descriptions,
in particular the descriptions of attributes and methods of a new
class, in 'C' can be quite challenging. And I'm afraid that you
would be correct in that assessment.
The rest of this document focuses on the issue of creating new
classes and describes the inter-relationships of the classes
provided to you by SHADOW.
The document 'ShadowLibraryMethods.doc' introduces SHADOW's
object-oriented system. Excepting some brief reiterations
and clarifications, that document will continue to contain
the main introduction to the object-oriented system. Therefore,
it is not merely highly suggested that you read the introduction
in ShadowLibraryMethods.doc, it is prerequisite that you do so.
To reiterate, aside from the functional aspects of SHADOW which
are introduced above, everything under SHADOW is an object. All
SHADOW objects begin with a struct CoreObject which can be found in
<shadow/core.h>. There are two fields in this structure, the
cob_useCount and the cob_class. The cob_useCount is used by the
resource tracking system and is covered earlier in this document.
The cob_class points to another OBJECT which contains the
information that is necessary to create, destroy, and otherwise
maintain this object. In short, this description (or blueprint or
class), contains a description of the attributes (or properties) of
the object and the methods (or functions, or verbs) of the object.
The relationship between "object" and "cob_class" is called the
"instantiation hierarchy". Object-oriented systems typically
describe classes in relation to other classes. The former group
of classes are called subclasses, and the latter are called
superclasses.
In the case of the "superclass hierarchy", there is a class which
has no superclass. This class is typically referred to as the
rootclass. In the case of SHADOW's "instantiation hierarchy", there
is no object without a cob_class, after all, an object without
a description is pretty hard to create! However, there is a
descriptor which, instead of terminating the instantiation
hierarchy, is a descriptor for itself. These descriptions
are referred to as METAs.
The instantiation hierarchy for SHADOW is three deep. For a summary,
please refer to the "instantiate hierarchy" entry in the Glossary.doc.
This three-level setup allows you, the programmer, to change
the methods of both your objects (by changing the method
descriptions in your classes) -and- your classes (by
changing the method descriptions in your metas).
There is more about METAs in ShadowLibraryMethods.doc, and I suggest
you read about them there as well. This document is more concerned
with how you actually create classes and metas, rather than what
classes and metas -are-.
Creating Classes
Classes contain two important descriptions -- attribute descriptions
and method descriptions. Attribute descriptions are very
straightforward. Method descriptions can become very complicated.
Classes are created by specifying the new class name, the superclass,
the new methods, and the new attributes like so:
CreateSubClass(NULL, ROOT_CLASS, META_CLASS,
MY_CLASS_NAME,
NULL,
myNewAttrs,
myNewMethods,
METHOD_END);
AttributeTags
The attribute description is a NULL-terminated array of ATTRIBUTE_TAGs.
Each element contains the following information:
typedef struct AttributeTag {
char *attag_name;
ULONG attag_size;
void *attag_default;
} ATTRIBUTE_TAG;
The attag_name is the name of the attribute. When you have an instance
of your created class you can access this attribute inside of the
instance by calling the FindAttribute() function passing in the
attag_name of the attribute you want access to.
The attag_size is the size of the particular attribute you want
created.
The attag_default is a pointer to a structure containing the
default values of the attribute. If this is not specified, the
default value for the entire attribute-structure will be zeroes.
Consider the following example. Say you wanted to create an object
that described a car. You want to keep certain information
about this car inside of an attribute.
First thing is to create a C structure:
struct CarInfo {
ULONG ci_wheelSize;
ULONG ci_steeringSide;
.
.
.
};
In this example, the ci_wheelSize refers to the size of the wheels
on the car and the ci_steeringSide refers to the side of the
car that the steering wheel is on, and so forth.
The next thing to do is to pick a name for the attribute. Please
note that the name is case-sensitive!:
#define ATTR_MY_CAR_INFO "My information about this car"
We can now create the ATTRIBUTE_TAG structure:
ATTRIBUTE_TAG myNewAttrs[] = {
{
ATTR_MY_CAR_INFO,
sizeof(struct CarInfo),
NULL
},
TAG_END
};
If we had wanted to we could have created default values for the
attributes so that when the class' instances are created, they
automatically have some non-zero values in them:
struct CarInfo defaultCarInfo = {14, LEFT, ...};
In which case the ATTRIBUTE_TAG structure becomes:
ATTRIBUTE_TAG myNewAttrs[] = {
{
ATTR_MY_CAR_INFO,
sizeof(struct CarInfo),
&defaultCarInfo
},
TAG_END
};
The creation of the class would necessarily follow this. Note that
this code makes the class a resource that will be freed when the
process defining the resource exits:
AddAutoResource(NULL, CreateSubClass(NULL,
ROOT_CLASS,
META_CLASS,
CAR_CLASS,
NULL,
myNewAttrs,
METHOD_END),
CAR_CLASS);
Once we had this class, we can create an instance of the class:
automobile = CreateInstance(NULL, CAR_CLASS, META_CLASS);
So, now that an actual car instance exists, you can access the car's
instance variables (the attributes of the instance), with the
following code:
{
struct CarInfo *ci;
ci = FindAttribute(automobile, ATTR_MY_CAR_INFO);
.
.
.
}
Once you have a pointer to the object's instance variables, you can
manipulate the structure, change variables, etc. However, this
pointer will only remain valid as long as the object it is assoicated
with remains valid. In other words, once you
"DropObject(automobile)", you may not use the "ci" variable again.
After all, the object may have been destroyed!
In addition, if this structure is to be shared across many processes,
you will want to protect access via the semaphore system (described
above). Fo all of my code I use the "ci" pointer to semaphore the
entire structure. You may find a more suitable way of doing the
same thing, if you wish. At anyrate, your code might look like:
{
struct CarInfo *ci;
ci = FindAttribute(automobile, ATTR_MY_CAR_INFO);
PSem(ci, SSEM_READ);
.
. // Various reads on the structure taking place.
.
VSem(ci);
}
The next step involves showing off a shortcut in SHADOW. SHADOW's
attributes, as defined by both META_CLASS and META_CLUSTER, are
created in the order that they are specified. This means that if
you create two attributes, one after the other, a pointer to
the first structure will easily lead you to a pointer to the second.
struct CarInfoExternal {
ULONG cie_wheelSize;
ULONG cie_wheelBase;
.
.
.
};
#define ATTR_CAR_INFO_EXT "Information about the car's externals"
struct CarInfoInternal {
ULONG cii_headClearance;
ULONG cii_cubicCapacity;
.
.
.
};
#define ATTR_CAR_INFO_INT "Information about the car's internals"
ATTRIBUTE_TAG myNewAttrs[] = {
{
ATTR_CAR_INFO_EXT,
sizeof(struct CarInfoExternal),
NULL
},
{
ATTR_CAR_INFO_INT,
sizeof(struct CarInfoInternal,
NULL
},
TAG_END
};
.
.
.
AddAutoResource(NULL, CreateSubClass(NULL,
ROOT_CLASS,
META_CLASS,
CAR_CLASS,
NULL,
myNewAttrs,
METHOD_END),
CAR_CLASS);
automobile = CreateInstance(NULL, CAR_CLASS, META_CLASS);
Assuming the above, the following:
{
struct CarInfoExt *ce;
ce = FindAttribute(automobile, ATTR_MY_CAR_INFO);
.
.
.
}
is identical to this:
{
struct CarInfo *ci;
ci = FindAttribute(automobile, ATTR_MY_CAR_INFO);
.
.
.
}
where "struct CarInfo" is declared as follows:
struct CarInfo {
struct CorInfoExt ci_cie;
struct CarInfoInt ci_cii;
}
As is apparent from this, a pointer to the first structure may very
well be useful to getting at other data. The advantages of having
separate attributes in the first place is to save space when using
explicitly declared default attributes, or if you wish to offer
the ability for subclasses to alter the default values for some
subset of your attributes without affecting some other subsets
of your attributes.
The above is an example of a time optimization. Instead of looking
up both attributes separately, only one was looked up, and the
second attribute's address was able to be hard-coded.
A similar improvement is possible when looking up attributes
frequently within methods. You can lock a global pointer to a class
and then lookup the "struct Attribute" which contains the necessary
information to later find the attribute locations within any instance
of that class. Note that you will have to keep that global
pointer "locked" in memory for the entire time that the Attribute
structure is being used.
DropObject(SetObject(&globalClass, FindShadowClass(CAR_CLASS)));
/*
* Note that FindShadowClass() returns a used class here.
*
* We use SetObject() to avoid race-conditions if
* more than one task tries to set the variable at
* the same time.
*/
struct Attribute *globalAttr = FindAttrDefn(globalClass,
ATTR_INFO);
/*
*
* We should not DropObject(globalClass) until we no longer
* need the attribute.
*/
.
.
.
Now, say you have some instance 'object', and it is the correct
class (object->cob_class = globalClass or somes subclass of
globalClass). Now you may merely add the value in the attr_offset
field within the attribute structure to the instance address and
arrive at the attribute within the object:
.
.
.
{
struct Info *info = (void *)(((ULONG)object) +
attr->attr_offset);
.
.
.
}
.
.
.
This will occur far faster than the FindAttribute() call. The two
disadvantages are the initial startup costs and shutdown costs, as
well as the necessity to keep the class "locked" in memory for the
entire time you have a globalAttr pointer.
.
.
.
/*
* CLEANUP!
*
* As we no longer need the globalAttr, we clear the globalClass
*/
DropObject(SetObject(&globalObject, NULL));
The only remaining thing to note about attributes is this: once the
class is created, the AttributeTag structures -and- the default
attribute structures are no longer needed by the system. The
system copies everything into memory when the class is created, so it
is very possible to have the AttributeTags and the default values
reside in a text file and load the text file at runtime, and then
purge the text file.
This means that changing the default attribute structure after the
class is created will NOT change the creation of future objects.
You can change these default attributes pretty easily, however. Here
is some example code. It takes a longword value within the default
object and updates it according to a hex value:
{
ULONG *ptr = GetDefaultAttribute(myClass, ATTR_WEBPAUSE, 0);
/*
* ptr may or may not exist! If ptr did not exist, then
* the attribute did not originally have a default
* attribute and the machine ran out of memory trying to
* allocate one, or the attribute itself did not
* exist within the passed class.
*/
if (ptr)
/*
* Convert the hex-string in args[0] and save the number
* into the longword pointer.
*/
stch_l(args[0], ptr);
}
GetDefaultAttribute() is defined as follows:
/*
* Looks for a default attribute object for the name'd
* definition.
*
* Returns either NULL (failure) or the default object + offset.
*
* Will create a default object if it doesn't exist.
* Will create a NEW default object if the superclass has the
* same default object. Note, subclasses -share- the same
* default attribute object with their superclasses
* [or, rather, vice-versa].
*
* Watched attributes do not -have- default values.
*/
void *GetDefaultAttribute(CLASS class, char *name, ULONG offset)
{
struct Attribute *attr = FindAttrDefn(class, name);
struct Attribute *temp;
struct ClasslessObject *obj;
if (!attr || (attr->attr_size & FLAG_ATTR_WATCHED))
return NULL;
temp = FindAttrDefn(class->ccl_superClass, name);
if (!(obj = attr->attr_value) ||
(temp && obj == temp->attr_value))
{
if (obj)
{
/*
* Create a new object for the subclass and copy
* in the values from the superclass' default
* object.
*/
if (!(obj = CreateObject(obj + 1, attr->attr_size)))
return NULL;
} else
{
/*
* If we have no default object, then the default
* values are all zero.
*/
if (!(obj = AllocMem(sizeof(struct ClasslessObject) +
attr->attr_size,
MEMF_PUBLIC | MEMF_CLEAR)))
{
return NULL;
}
/*
* Setup the new object with a size and usecount
*/
obj->clb_size = sizeof(struct ClasslessObject) +
attr->attr_size;
obj->clb_useCount = 1;
}
/*
* Save the new default object into the class --
* drop any old default objects there, and make sure
* the operation is single-threaded.
*/
DropObject(SetObject(&attr->attr_value, obj));
}
/*
* Returns a pointer into the default object so that you
* cn directly dork with the values.
*/
return (void *)(((ULONG)(obj + 1)) + offset);
}
MethodTags
As mentioned above, attribute creation is fairly straightforward --
when compared to method creation. And that's because creating a
method involves a lot more than just creating a couple of structures.
It involves considering the types of methods you have available --
synchronous, asynchronous, call-by-port, call-by-process. It also
involves creating the actual functions and function argument tags,
and the process classes (and instances) that you might need to run the
method inside of.
This presents the creation of several types of methods from the
function-level up. However, you should realize that the process is
not always so straightforward.
A method's function always has at least the following four arguments:
struct IPCMessage *msg;
OBJECT object;
META class;
char *MethodID;
The <msg> parameter is used by the asynchronous method sends. It
is provided for handling some strange resource-tracking cases that
are discussed below. This parameter is often NULL.
The <object> parameter is the object on which the selector is being
invoked. This parameter is never NULL.
The <class> parameter is a pointer to the class of the object that
actually handles the given MethodID. Remember, every object is an
instance of some class. Classes live in a class-hierarchy from which
they inherit other class' methods. Therefore, when a particular
method is invoked, the method itself might be handled by the object's
class, or some superclass of the object's class. This parameter
distinguishes -which- class handles the passed selector....
The <MethodID> parameter is a pointer to a system-string which
corresponds to the selector for this particular method. It is a
useful way to allow multiply different methods to converge on one
of your functions, and then diverge when the method is sent off
to the superclass for further handling.
Obviously, typing all of this information into each function would
be tedious. The includes defines the macro "METHOD_ARGS", which
contains these argument definitions. The ability of your compiler to
handle this shortcut may differ from mine....
Here is a sample method-function definition:
void ProcTestMethod(METHOD_ARGS)
{
BPTR oldoutput;
oldoutput = SelectOutput(Open("CONSOLE:", MODE_OLDFILE));
VPrintf("Called method: <%s> ", (ULONG *)&MethodID);
VPrintf("in process <%s>\n", (ULONG *)&FindTask(NULL)->
tc_Node.ln_Name);
VPrintf("\tin Class <%s> ", (ULONG *)&(CurrentProcess()->
cob_class->meta_name));
VPrintf(((msg)?"Asynchronously\n":"Synchronously\n"), NULL);
oldoutput = SelectOutput(oldoutput);
Close(oldoutput);
CallSuper();
}
This is a relatively simple method-function that the included perftest
program uses to verify nearly all of the method-invoke possiblities
(by function, synchronous msg, asynchronous msg, etc).
Once you have this function written, you need to create an
ARGUMENT_TAG structure which describes the addition arguments that
the method tags (above the four described above). Because this
method does not take any extra arguments, we will postpone talking
about ARGUMENT_TAGs until we have a more appropriate example.
Now we have to create a name for the selector of this method:
#define METH_MY_NEW_PROC_TESTER "A string of your own choosing"
The next thing to do is to add the method into your class' METHOD_TAG
list. The METHOD_TAG list has eight different variables. We take
each in turn.
The first field in the METHOD_TAG structure is the mtag_MethodID
field. This field is very straightforward -- it should always
carry a non-NULL field that is a pointer to the string which is
to represent the method's selector.
The next two fields are named: mtag_procObject and mtag_defnObject.
Although they have similar names, their meanings are very different.
The mtag_defnObject is used by the system to ensure that the program
(or some other loaded code, like a library) which contains the
definition of the function and the ARGUMENT_TAG in its seglist,
does NOT disappear until all reference to the function by any
methods in any classes are eliminated.
mtag_defnObject:
Here's the basic problem. Program A has loaded a method, M. Program
A now defines a public class C which has the method M.
Program B now comes along, creates an instance I of class C. Program
A quits. Program B tries to invoke method M of instance I, which used
to reside in Program A, but has been since eliminated. This is
clearly not good. The same thing might happen for a library base
(although the OpenCnt of the library base should be enough to
catch all legal errors in this respect).
What we do in the instance of a library is to define a global,
classless object, and use the pointer to this global object as the
defn_object:
struct ClasslessObject
globalLibraryObject = {0, 0, 0};
In the EXPUNGE code, you'll want to verify that the
globalLibraryObject.cob_useCount is zero. If it is a non-zero
integer, then someone still has a pointer to an object that has
a class (or some superclass thereof) that points to a method in
your library. Thus, your method may still be called, and you
should refuse to expunge your library.
This is a distinct advantage over BOOPSI where class libraries are
difficult to manage because of the various resource tracking issues.
However, creating methods inside of libraries is not likely to be
the most frequent thing you do. Far more frequently you will be
creating classes inside of programs. And for this, you will need to
retrieve the CurrentProcess() -- a pointer to your process' object
that was created during the InitOOProgram() call (the FIRST function
you should always call after your OpenLibrary("shadow.library", 5) is
InitOOProgram()!!! -- see the Process Class discussion below).
/*
* CurrentProcess() is a macro defined in <shadow/shadowProto.h>
*
* This object is not resource tracked, so you need not
* DropObject() this processObject after you're done using it.
*/
processObject = CurrentProcess();
Once you have this pointer, you modify each of your MethodTags to
have the mtag_defnObject point to the CurrentProcess() object.
This is tedious, and the funcion SetupMethodTags() is provided to
help you in this respect. We will talk more about SetupMethodTags
when we begin the discussion of mtag_procObject.
It stands to reason, as well, that you need to be in the process
that the program runs in! In otherwords, FindTask(NULL) had
better be your program's task, or you are going to end up with the
wrong process!
At anyrate, once your class is created, your program cannot exit
before the methods are no longer callable via this class because the
last call your program should make before the CloseLibrary(ShadowBase)
is the RemoveCurrentProgram() call, which will not return until all
references to your CurrentProcess() go away. Again, you have a safe
way in which to define public classes and ensure no one has a pointer
to your internal methods when your program ends up quitting and
returning its resources to the system.
mtag_procObject:
The mtag_procObject is used to help determine what process the
object's method needs to be run in. SHADOW is very flexible in its
ability to run methods in other processes. You can ask for something
as simple as a function call, or something much more complicated,
like "invoke a method by creating a process of the given named class;
then, replace the named class with a pointer to the actual class
pointer so that the next invocation no longer has to look the class
up in the class list."
While the mtag_defnObject is always a SHADOW object, the
mtag_procObject can be any number of things. Some of the mtag_flags
are used to decide what the mtag_procObject is pointing to.
A valid mtag_procObject is one of five values:
a) it is a pointer to a process object.
b) it is a pointer to an IPCPort.
c) it is a pointer to a process class
d) it is a pointer to a struct MethInvokeSpec <shadow/method.h>
e) it is NULL
mtag_flags:
There are five flags in the mtag_flags which control the behaviour of
these various possibilities:
METH_FLAG_OBJECT - The mtag_procObject is interpreted as the
destination process object.
METH_FLAG_PORT - The mtag_procObject is interpreted as an
IPCPort to which method-messages are sent.
METH_FLAG_CLASS - The mtag_procObject is interprted as a
class of process objects. This class is
instantiated, and the resulting process
object is used as the destination for
that particular method invocation.
METH_FLAG_OBJECT_AS_DEST
- The object the method is being invoked on is
itself used as the destination process
object. This is particularly useful for
process methods which must (for reasons of
port allocation or other signal-bit
allocation, etc.) be run in the process
with which the process-object corresponds.
METH_FLAG_SPEC - The mtag_procObject is interpreted as a
pointer to a struct MethInvokeSpec
[see: <shadow/method.h>], the complete
meaning of which depends on the above four
flags in the following manner:
METH_FLAG_SPEC | METH_FLAG_OBJECT
mis_instanceName is the name of the process object to use
as the destination object.
mis_className is the class name of the class of the
above process object.
mis_metaName is the meta's name of the meta of the
above class. Usually this is
META_CLASS.
METH_FLAG_SPEC | METH_FLAG_PORT
mis_instanceName is the name of the IPCPort to use as the
destination port. This port will NOT
be created, but must exist when the
method is called.
mis_className is NULL
mis_metaName is NULL
METH_FLAG_SPEC | METH_FLAG_CLASS
mis_instanceName is NULL
mis_className is the class name of the process to
create.
mis_metName is the meta's name of the meta of the
above class. Usually this is META_CLASS.
METH_FLAG_SPEC | METH_FLAG_OBJECT_AS_DEST
the METH_FLAG_SPEC is meaningless in this case as the
METH_FLAG_OBJECT_AS_DEST never uses the mtag_procObject
field.
When the process/port is looked-up/created,
the process/port may replace the
MethInvokeSpec structure, eliminating the
need to lookup the object/class/port again.
To do this, set the SPEC_SAVE_BINDING flag
in the mis_flags field of the MethInvokeSpec
structure.
A NULL mtag_procObject is treated differently depending on the
mtag_flags and the mtag_threadstat. As the mtag_threadstat hasn't
been discussed yet, though, it makes sense to do that first.
mtag_threadstat:
Where the mtag_flags field describes how the mtag_procObject is
interpreted during a method call, the mtag_threadstat determines how
the method is actually invoked. The following flags are defined in
<shadow/message.h>:
INVOKE_CALL - Calls method as function.
INVOKE_SYNC - The method is invoked as a synchronous message
to the method's mtag_procObject, unless the
calling process belongs to a subclass
of the destination's process' class,
in which case the method is called as a
function.
INVOKE_ASYNC - The method is invoked as an asynchronous
message to the method's mtag_procObject,
unless the calling process belongs to a
subclass of the destination's process'
class, in which case the method is called
as a function.
INVOKE_FORCE - Used in onjunction with above two flags. See
next two flags.
INVOKE_FORCE_SYNC - The method is sent as a synchronous message
unless the calling process is exactly
equivalent to the destination process, in
which case the method is called as a funcion.
INVOKE_FORCE_ASYNC- The method is ALWAYS sent as an asynchronous
message. Irregardless of calling/destination
process.
INVOKE_IGNORE_PROCESS
- It is possible for programers to call DSM()
and its associated helper functions
(eg. DoShadowInProcess()) to override the
process that the method is handled in.
Setting this flag in the mtag_threadstat
prevents the mtag_procObject from being
overridden with the INVOKE_WITH_PROCESS
flag that can be sent to DSM(). See the
Autodoc for DSM() for more information.
As you can see, there are a variety of ways in which to call a method.
You can call a method asynchronously to a named IPCPort (mtag_flags
would be METH_FLAG_PORT | METH_FLAG_SPEC, mtag_threadstat would be
INVOKE_FORCE_ASYNC, and mtag_procObject would be a pointer to a
MethInvokeSpec structure) or invoke a method as a simple function
call (mtag_flags would be METH_FLAG_OBJECT, mtag_threadstat would be
INVOKE_CALL, mtag_procObject would be NULL). Many of the callable
attributes are overridable at method invoke-time, so you can
determine how the method is invoked both in the method definition
(which is being discssed here) and method invocation (which will be
described briefly here, and is described more fully in the DSM()
autodoc).
When mtag_procObject is NULL, there are several possible reactions by
SHADOW, depending (as mentioned above) on the mtag_flags and
mtag_threadstat settings:
INVOKE_CALL - NULL mtag_procObject is ignored, call
proceeds as usual.
mtag_flags = METH_FLAG_OBJECT:
INVOKE_[FORCE]_SYNC - Method is only successfully called if the
calling task has no associated SHADOW
object. Otherwise DSM() or its helper
functions (eg. DoShadow()) returns NULL.
If the calling task has no associated
SHADOW object, the method is invoked as
a function call.
INVOKE_[FORCE]_ASYNC- Similar logic to the INVOKE_SYNC logic
above except that INVOKE_FORCE_ASYNC where
the mtag_procObject is NULL will force an
asynchronous method-invoke message to be
sent to the calling task's SHADOW-method
port, stored in the sp_port field of the
struct ShadowProcess structure in the
ATTR_SHADOWPROCESS attribute of the
process' object. If there is no current
task, the message should be called as a
function.
mtag_flags = METH_FLAG_PORT
-or-
mtag_flags = METH_FLAG_CLASS
A NULL port/class (mtag_procObject), except when mtag_threadstat
is INVOKE_CALL, will cause the method invocation to fail. DSM(),
or its helpers (eg. DoShadow()), will return NULL.
mtag_flags = METH_FLAG_OBJECT_AS_DEST
A NULL mtag_procObject is meaningless as the object argument of
the DoShadow() call (or the first pointer in the arguments
array to DSM()) is used as the process object to which to send
the method.
mtag_flags | METH_FLAG_SPEC
Not specifying a pointer to a MethInvokeSpec structure in the
mtag_procObject when the flag METH_FLAG_SPEC is set in the
mtag_flags field is a programming error. Your class will not
be created under these conditions. mtag_procObject MUST be
non-NULL, and had BETTER point to a MethInvokeSpec structure.
This MethInvokeSpec structure is copied when the class is
created, so you need not have your MethInvokeSpec structures
lying around after your classes are created.
mtag_priority
This field is only used by the PATCHER_CLASS. It is ignored
by the class, though you set it to zero for future
compatibility.
mtag_method
This should contain a pointer to the function which implements
the method.
mtag_arguments
This should point to an ARGUMENT_TAG array that describes the
arguments the method's function takes (this allows asynchronous
methods to have their arguments correctly resource-tracked),
and describes the return value of the function. If the function
does not return anything, and there are no arguments above and
beyond the METHOD_ARGS default arguments of all methods, then
this field may be NULL. The pointer to this supplied
ARGUMENT_TAG is -not- copied, although the contents of the
ARGUMENT_TAG MUST remain constant. The ARGUMENT_TAG should
be considered a part of the method's function, rather than as
an extension to the data that describes the method (METHOD_TAG).
That data (the METHOD_TAG) is copied and altered appropriately
when the class is created.
Given this information, here is an example METHOD_TAG which enables
the associated class to invoke the METH_MY_NEW_PROC_TESTER method
and have this translated into a function call to the ProcTestMethod()
function which was described several pages ago:
METHOD_TAG myNewMethods[] = {
.
.
.
<other methods for this class>
.
.
.
{
METH_MY_NEW_PROC_TESTER,
NULL, NULL,
INVOKE_CALL,
METH_FLAG_OBJECT, 0,
(METHODFUNCTYPE)ProcTestMethod,
NULL
},
TAG_END
};
Notice that this method tag does not specify an mtag_procObject nor
an mtag_defnObject. For this particular METHOD_TAG item, the
mtag_defnObject needs to be set to the CurrentProcess() and the
mtag_procObject can remain NULL. However, it is quite possible that
other METHOD_TAG items need to have their mtag_procObject be
something other than NULL, and it is perfectly fine to specify
an mtag_procObject as any process object you wish when the
mtag_threadstat is INVOKE_CALL and the METH_FLAG_OBJECT is set.
As the CurrentProcess() is only available at runtime, you can either
iterate through the METHOD_TAG array and setup the defnObject and
the procObject as you wish, or you can use the supplied
SetupMethodTags() routine.
SetupMethodTags takes three arguments -- the METHOD_TAG array, the
procObject to be used in each element of the array, and the defnObject
to be used in each element of the array. If either of these last two
arguments are specified as "-1", then the results of CurrentProcess()
are used. If the item already has a defnObject or procObject
specified (non-NULL), then that field remains untouched. This is
especially useful when using the MethInvokeSpec structure as the
mtag_procObject -- this is a static structure which can be set at
compile-time....
Please note! When setting the elements of this array, SetupMethodTags
does not UseObject() any of the objects. Therefore, be careful if
you ever define a new type of MetaClass which allows for classes to
be defined asynchronously to the caller, as the mtag_procObjects and
the mtag_defObjects will not be correctly resource-tracked.... This
is not a problem for the default CreateSubClass() call which 99% of
you will be using.
The corresponding SetupMethodTags() for this METHOD_TAG structure
looks as follows:
/*
* Set all procObjects and defnObjects to CurrentProcess();
*/
SetupMethodTags(myNewMethods, (void *)-1, (void *)-1);
The creation of the class would then follow this. Note that this
code makes the class a resource that will be freed when the
process defining the resource exits -- also, we use the "myNewAttrs"
ATTRIBUTE_TAG which is defined several pages back:
AddAutoResource(NULL, CreateSubClass(NULL,
PROCESS_CLASS,
META_CLASS,
MY_PROC_CLASS,
NULL,
myNewAttrs,
myNewMethods,
METHOD_END),
MY_PROC_CLASS);
We now possess a new MY_PROC_CLASS with some attributes and some methods.
Of course, to call a method, we first need an instance!
process = CreateInstance(NULL, MY_CAR_CLASS, META_CLASS);
And now to call the new method:
DoShadow(process, NULL, METH_MY_NEW_PROC_TESTER, METHOD_END);
.
.
.
/*
* Don't forget your cleanup!
*/
RemoveObject(process);
The DoShadow() call is very straightforward -- first the object, then
the class at which to begin looking for the method (NULL signifies
object->cob_class, which is the way you'll call 80-90% of all your
methods), then the selector string, then any arguments that are
expected by the method, and then a METHOD_END terminator.
A few things need to be pointed out here. First, you need not
specify all your arguments before the METHOD_END. Arguments
which are not specified are set to zero by the method calling code.
This allows you to create new versions of your software, adding new
arguments to old methods, without having to worry about anybody
else's additions to your classes! Provided, of course, that NULLs
default to the old version's behaviour....
Second, not specifying the METHOD_END terminator is a bug in your
code -- don't do it, even if it does work....
Third, it is VERY IMPORTANT that no argument that you pass into the
method has the same value as METHOD_END -- 0x80000001 -- or,
alternatively, that no two adjacent WORD arguments, combined, form a
0x80000001. This value was picked so as to be a very high negative
number, an odd number (impossible address), and just plain unlikely
to cause problems. You will have to become clever if this is not the
case for you.... Someday, a language might hide this detail from you,
but you'll have to live with it for now. If this is just not
satisfactory, then let me suggest an alternative. For all those
items that require resource tracking -- strings, objects, structure
pointers, pass those as separate arguments, and for all flags and
numbers, etc., pass those arguments in a single method-argument as a
TagArray structure pointer. There is a 'TAGL' type of argument that
will allow you to do just such a thing....
Fourth, the CreateSubClass() and the CreateInstance() both have a
METHOD_END terminator, and it now becomes clearer why -- they
both cleverly use the passed stack arguments as arguments into a
method call which creates either an instance of a class or a
class itself....
There is no reason why more than one method can call the same
function. The following is perfectly reasonable:
#define METH_MY_NEW_PROC_TESTER_1 \
"A string of your own choosing 1"
#define METH_MY_NEW_PROC_TESTER_2 \
"A string of your own choosing 2"
METHOD_TAG myNewMethods[] = {
{
METH_MY_NEW_PROC_TESTER_1,
NULL, NULL,
INVOKE_FORCE_ASYNC,
METH_FLAG_OBJECT, 0,
(METHODFUNCTYPE)ProcTestMethod,
NULL
},
{
METH_MY_NEW_PROC_TESTER_2,
NULL, NULL,
INVOKE_SYNC,
METH_FLAG_PORT, 0,
(METHODFUNCTYPE)ProcTestMethod,
NULL
},
TAG_END
};
.
.
.
extern struct IPCPort *somePortSomeplace;
/*
* Set all procObjects and defnObjects to CurrentProcess();
*/
SetupMethodTags(myNewMethods, (void *)-1, (void *)-1);
/*
* oops, but reset the port.
*
* We could have the line below and the line above switch
* places as well!
*/
myNewMethods[1].mtag_procObject = somePortSomeplace;
/*
* Create the class.
*/
AddAutoResource(NULL, CreateSubClass(NULL,
PROCESS_CLASS,
META_CLASS,
MY_PROC_CLASS,
NULL,
myNewAttrs,
myNewMethods,
METHOD_END),
MY_PROC_CLASS);
AddAutoResource(NULL, process = UseObject(
CreateInstance(NULL,
MY_CAR_CLASS,
META_CLASS)),
NULL);
/*
* Send off some methods....
*
* The first is called asynchronously.
* The second is called synchronously and is handled by the
* "somePortSomeplace" IPCPort. The same function
* (ProcTestMethod()) should be called regardless of
* this, however.
*/
DoShadow(process, NULL, METH_MY_NEW_PROC_TESTER_1, METHOD_END);
DoShadow(process, NULL, METH_MY_NEW_PROC_TESTER_2, METHOD_END);
.
.
.
/*
* Hey, don't forget the cleanup!
*/
DropObject(process); /*
* Matches my UseObject()
* above
*/
/*
* Oh, and I haven't shown shutdown recently....
*/
RemoveCurrentProgram(NULL); /* removes the process and the
process' class as they were added
as autoresources.
*/
CloseLibrary(ShadowBase);
.
.
.
ArgumentTags
The remaining mystical portion to methods (if you've been following
everything else I've written) are the ARGUMENT_TAGs.
ARGUMENT_TAGs describe the arguments that a method receives, above
and beyond the nominal METHOD_ARGS, which ALL methods receive.
While it is only the number and size of the arguments which
interests the INVOKE_CALL and INVOKE_SYNC types of calls, the type
of the argument is very important when invoking a method
ASYNChronously -- this is how the arguments get properly resource-
tracked across asynchronous tasks! In addition, if resources are
returned from an asynchronous invocation, they need to be thrown
away -- the ARGUMENT_TAG provides for this behaviour as well.
An ARGUMENT_TAG, therefore, comes in two parts -- the first part
describes each of the arguments -- the type, the size that is
required for them on the stack (yes, you can pass a structure
to a method by-value), and any additional information that
might be needed (the size of a pointer, the size of each element
in an array, etc.). The second part serves as both the ARGUMENT_TAG
terminator (the TAG_END) and contains the information about the
returned resource (the type and the additional information fields,
the size on the stack is not applicable as DSM() only returns
a single long-word value in d0).
The following is taken from the AutoDoc for SetMethodArgs(), a
function which you will not need to use in all likelihood, but,
nevertheless, the place where information about ARGUMENT_TAGs
was previously kept. Note that the definition for the AttributeTag
structure resides in <shadow/misc.h>:
If the method-function returns a variable that needs to be kept
track of, the last ArgumentTag structure (whose at_tag should be
zero) in the array should contain information for async. method
sends to safely deallocate or otherwise resource track the
returned value.
Valid returns are:
SHADOW_RETURN_BLANK 0
SHADOW_RETURN_OBJECT 1
SHADOW_RETURN_STRING 2
SHADOW_RETURN_PORT 3
SHADOW_RETURN_TAGL 4
SHADOW_RETURN_PTR 5
SHADOW_RETURN_MRFO 6
These values should be stored in the at_size field of the last
ArgumentTag array item..
For SHADOW_RETURN_PTR, the size of the pointer should be stored
in the at_flags field of the same item, for SHADOW_RETURN_TAGL,
the size of each array item should be stored in the at_flags field
of this last item, and for SHADOW_RETURN_MRFO, the offset of the
returned data from the object in question should be placed in the
at_flags field
Normally, that is, in every case except the last item, the system
supports a number of at_tag values:
'MRFO':
used for object values which add at_flags to
the passed object pointer.
at_size should be four
at_flags -- the offset of the passed data from the object ptr.
Note that MRFO exists for system use, however you are free to
use the item as well -- but it's of little to no use, just
pass the object instead!
'JOBJ':
Used for an object.
at_size should be four
at_flags should be one of:
SHADOW_OBJECT
SHADOW_CLASSLESSOBJECT
SHADOW_META
SHADOW_CLASS,
SHADOW_CLUSTER
SHADOW_COMPOSITE
though this is more for peace of mind than necessity.
'JMSG':
Used for a message creted by MessageMaker()
at_size should be four
at_flags should be zero
'JSTR':
Used for a system string.
at_size should be four
at_flags should be zero
'PORT':
Used for a ppipc port.
at_size should be four
at_flags should be zero
'TAGL':
Used for a NULL terminated array.
at_size should be four.
at_flags should be the size of each array item
'RTRN':
reserved by the system, don't use.
'APTR'
a pointer to some memory.
at_size should be four.
at_flags should be zero if you don't want to copy the data to
another block when sending async., or should be the size of
the data pointed to if you do.
Create you own 4 character constant:
at_size should be an even number -- you can pass arbitrarily
large structures on the stack, this way.
at_flags ignored.
If we examine closely, a few limitations crop up. For one thing,
there doesn't seem to be a way to add another "type" of argument that
would require a different kind of resource-tracking. This is TRUE --
please send me your requests!
Second, you can't pass a structure pointer that point to a structure
of more than 65535 bytes (at_flags is a UWORD value). If you need to
pass a 64k or bigger structure, feel free to prepend the three
long-word values which would be required for a struct
ClasslessObject, set the classless object's size to the final size of
the larger structure, and pass the large structure as an object.
The structure will thus not be copied between asynchronous tasks
(saving time and space). If you need to copy the object, use the
original structure (without the struct ClasslessObject header) in a
call to CreateObject() like this:
newObject = CreateObject(bigStructurePointer,
sizeof(BigStructure));
This will create a new ClasslessObject structure with the values found
in the passed bigStructurePointer copied into the returned object.
The method call would proceed:
DoShadowAsync(object, NULL, SOME_METHOD, newObject, METHOD_END);
/*
* No Longer need this!
*/
DropObject(newObject);
Similarly, the array item in a TAGL can not be larger than 65k. The
array can be larger, just not each individual item....
The following example introduces ARGUMENT_TAGs in a real-world
application. The code is analogous to the METH_INIT code for the
ROOT_CLASS. This method (as the browser indicates) takes a single
parameter -- a 'JSTR'. This string is used to add the object to
some kind of global (avl) tree structure. If the string is NULL,
then the object's address is used instead. For the ROOT_CLASS'
METH_INIT, the global tree ends up being the object's class'
ATTR_INSTANCETREE tree, not a global tree as in this example:
METHOD_TAG carMethods[] = {
.
.
.
{
METH_CAR_REGISTER
NULL, NULL,
SHADOW_MSG_CALL,
METHOD_FLAG_PROC, 0,
RegisterCarMethod,
REF_RegisterCarMethod
},
TAG_END
};
.
.
.
/*
* The MY_CAR_CLASS methods.
*
* METH_CAR_REGISTER:
*/
ARGUMENT_TAG REF_RegisterCarMethod[] =
{
{'JSTR', 4, 0}, /* The one argument */
{TAG_END, SHADOW_RETURN_OBJECT, 0}
/* The return spec. */
};
OBJECT RegisterCarMethod(METHOD_ARGS, char *string)
{
/*
* This method "swallows" the object passed in and
* either returns or removes it. Therefore, we must
* transfer the object out of any asynchronous messages
* we might receive.
*
* The "0" is used because "object" is the "zeroth"
* argument to this method. "class" is first, "MethodID"
* is second, "string" is third, etc.
*/
IPCARGTransfer(msg, 0);
/*
* If there is a name, add the object to the tree with that
* name, otherwise add the object using the object's address.
*/
if ((!string && AddWatchedTreeNode(globalTree
object,
(long)object)) ||
(string && AddWatchedTreeStringNode(globalTree,
object,
string)))
{
return object;
}
/*
* Failure case, cleanup!
*/
RemoveObject(object);
return NULL;
}
.
.
.
/*
* Creating the MY_ROOT_CLASS
*/
SetupMethodTags(rootMethods, (void *)-1, (void *)-1);
myRoot = CreateSubClass(NULL, ROOT_CLASS, META_CLASS,
MY_CAR_CLASS,
NULL,
NULL,
/* No new attrs */
rootMethods,
METHOD_END);
AddAutoResource(NULL, myRoot, MY_ROOT_CLASS);
.
.
.
/*
* No Cleanup necessary!
* Just call RemoveCurrentProgram() and get out!
* The MY_ROOT_CLASS wil be -terminated- automagically.
*/
RemoveCurrentProgram(NULL);
CloseLibrary(ShadowBase);
.
.
.
This example uses one argument specification -- a 'JSTR' -- and
then specifies that the method returns an object. We can call
this method using the following (assuming this code is run
somewhere between the CreateSubClass() and the
RemoveCurrentProgram() calls in the above code example!):
/*
* Create the object
*/
auto = CreateInstance(NULL, MY_ROOT_CLASS, META_CLASS,
METHOD_END);
/*
* Send off the method.
*/
auto = DoShadow(auto, NULL, METH_CAR_REGISTER, METHOD_END);
-or-
auto = DoShadow(auto, NULL, METH_CAR_REGISTER, "some object",
METHOD_END);
/*
* Cleanup!
*/
RemoveObject(auto);
This method is also safe to call asynchronously!
DoShadowAsync(auto, NULL, METH_CAR_REGISTER, "some object",
METHOD_END);
/*
* Cleanup!
*
* It is possible for the METH_REMOVE method to be called
* twice in this case....
*/
RemoveObject(auto);
Indeed, ARGUMENT_TAGs are most heavily used for the asynchronous case.
The DoShadowAsync() attempts to invoke an asynchronous method on the
auto. This causes DSM() to create an IPC message with several items
of interest -- the auto object, the string argument, and the return
value are the only items that matter for this example.
The auto object has its cob_UseCount incremented via UseObject().
The string argument is created with the UseString() call.
The return value is set to return an object (specifically, the
ipc_Id is set to 'JOBJ'). The message is then sent off to the
destination process.
When the destination process receives the message, the method ends
up being called (within ParseShadowMessage()). The method
specifically removes the "object" (the "auto") from the message.
This method is defined as "swallowing" the auto-object, so the
message is updated to reflect this. The "auto" is then added to some
globalTree. If the addition is successful, the "auto" is returned.
Otherwise the "auto" is Remove()'d from the system.
When the auto is returned, it is placed into the return slot in the
message. ParseShadowMessage() then attempts to return the message.
However, asynchronous messages do not have a ReplyPort, so
ParseShadowMessage() ends up calling JunkIPCMessage() which destroys
each item's reference and then destroys the message.
In order, then, the return object is Drop()'d via DropObject(), the
auto-object is ignored (remember the IPCARGTransfer()!), the
string is Drop()'d via DropString(), and the message is destroyed.
Note, if you hadn't specified the argument in the ARGUMENT_TAG,
char *string would have ended up being 0x80000001 (or worse, if
you had more arguments, those other arguments might be the return
address of the function or stored registers, etc.). It is very
important that your ARGUMENT_TAG matches your method's actual
parameter specification. In addition, if your SHADOW_RETURN_*
field is incorrect (or left unspecified), improper resource
tracking of the return object might take place. For instance, if
the ARGUMENT_TAG were specified as follows:
ARGUMENT_TAG REF_RegisterCarMethod[] =
{
TAG_END
};
Then the passed string would take on the value of 0x80000001 and the
returned object in the asynchronous case would be "lost." It would
never be Drop()'d from the system, and thus repeated use of the
function (asynchronous use!) would cause memory loss and
possibly memory fragmentation.
Many macros have been defined in <shadow/misc.h> which allows you
to build ARGUMENT_TAGs more easily. The correct
REF_RegisterCarMethod[] would look as follows:
ARGUMENT_TAG REF_RegisterCarMethod[] =
{
ARG_STR,
RET_OBJ
}
Which is to say, one string argument, and the return of an object.
Using these macros, we define an example which uses all of the
important types of method arguments:
ARGUMENT_TAG REF_RaceCarMethod[] =
{
ARG_OBJ,
ARG_STR,
ARG_TAGL(sizeof(struct TagItem)),
ARG_MESG,
ARG_PORT,
ARG_INT,
{'stuf', sizeof(struct StockCar), 0},
ARG_PTR(0),
RET_NORMAL
}
/*
* Note that the error message field is actually never
* deleted, this should be done by the caller. Note
* also that deleting the message even after an
* asynchronous method invocation works because the message
* is -copied- for the asynchronous send, and then the copy is
* magically deleted after the method-function is done
* executing.
*
* ParseShadowMessage() is usually called with the
* SHADOW_RETURN_MSG_NEVER, but this is an example of an
* alternative usage pattern. Please refer to the AutoDocs
* for more information about ParseShadowMessage()'s flags.
*/
BOOL RaceCarMethod(METHOD_ARGS,
OBJECT registry,
char *name,
struct TagItem *tags,
struct IPCMessage *error,
struct IPCPort *port,
long failure_probability,
struct StockCar sc,
void *garbage)
{
if (!...use all the arguments to race the car...)
{
if (port && error)
{
error->ipc_Msg.mn_ReplyPort = GlobalReplyPort;
if (PutIPCMessage(port, error);
{
/*
* A simplified version of code to retrive
* the message back from the error handling port.
*/
WaitPort(GlobalReplyPort);
GetMsg(GlobalReplyPort);
} else
ParseShadowMessage(error,
SHADOW_RETURN_MSG_ALWAYS);
} else if (error)
ParseShadowMessage(error, SHADOW_RETURN_MSG_ALWAYS);
return FALSE;
}
return TRUE;
}
There are four specific things I want to mention with this example.
First, the ARG_TAGL need NOT have an item size identical to the
size of struct TagItem. Any array of items whose last item begins
with a zero longword is considered a valid ARG_TAGL.
Second, the ARG_MESG parameter is a message which holds a "preparsed"
version of a method call. You can create a message to pass into
this method by using the following as an example:
errorMessage = PreParseShadow(car, NULL, METH_CAR_ERROR,
"Bad Race!",
METHOD_END);
The errorMessage contains a message which can be sent to another port
or Parse()d out at some later time. The METH_CAR_ERROR method is not
actually called; the returned message, however, contains all the
pertinent information to call the method if you later decide to. To
get rid of the message, you can either call ParseShadowMessage:
ParseShadowMessage(errorMessage, SHADOW_RETURN_MSG_NEVER);
which will invoke the method, and then destroy the errorMessage; or
you can call JunkIPCMessage to destroy the message:
JunkIPCMessage(errorMessage);
Third, the "('stuf', sizeof(struct StockCar), 0}" is an example of
how to pass large structures on the stack to a method. To operate
properly on 68000 based machines, the sizeof() had better be even.
For best performance, it should be at least long-word aligned.
Lastly, the ARG_PTR(0) means to pass a pointer to the function, but
not to copy the data to another pointer if resource tracking would
have normally required it (ie: asynchronous methods). If you wanted
to pass the StockCar structure in by pointer instead of on the
stack, you could conceivably use:
ARG_PTR(sizeof(struct StockCar)),
instead of:
{'stuf', sizeof(struct StockCar), 0},
and define the method as
., /* Args deleted! */
.,
.,
struct StockCar *sc,
void *garbage)
instead of:
., /* Args deleted! */
.,
.,
struct StockCar sc,
void *garbage)
This alternative should run faster than requiring stack copying as in
the original example.
Assumeing you had an object of the correct class, the original method
invocation might look as follows:
errorMsg = PreParseShadow(car, NULL, METH_CAR_ERROR,
"Bad Race!",
METHOD_END);
tags[0].ti_Tag = CAR_FORMULA;
tags[0].ti_Data = 2;
tags[1].ti_Tag = TAG_END;
DoShadow(car, NULL, METH_CAR_RACE, registration, "Road Runner",
tags, errorMsg, NULL, 85, myStockCarStructure, NULL,
METHOD_END);
JunkIPCMessage(errorMsg);
This would work regardless of whether the method was defined to run
synchronously (as a function call or a synchronous message) or
asynchronously. This independence allows you to create code that
will run in a wider array of circumstances, and allow you to further
tune your application by running various parts of your program in
separate processes.
CreateInstance:
I have been using some wrapper functions in all of my method
discussions -- CreateInstance and CreateSubClass. In reality, they
are really method calls. CreateInstance() actually contains two
methods as in the following pseudo-code:
CreateInstance(privateClass, className, metaName, <..args..>)
{
OBJECT obj;
if (!UseObject(privateClass))
{
created = TRUE;
if (metaName)
privateClass = FindInstanceOfMeta(className, metaName);
else
privateClass =
FindNodeWatchedTree(&ShadowBase->sb_metaTree,
metaName);
}
/*
* Create the object's memory.
*/
obj = DoShadow(privateClass, NULL, METH_CREATE, METHOD_END);
DropObject(privateClass);
/*
* Make it a real object.
*/
ret_val= DoShadow(obj, NULL, METH_INIT, <..args..>);
return ret_val;
}
The first method (METH_CREATE) actually creates memory for the
object to exist in. In addition, it sets up the class pointer for
the object. However, it is not, at this point, a valid object. Were
something to fail here, or in the METH_INIT, the only way to get the
object to actually go away is to do the following:
DropObject(UseObject(obj));
This is in direct contrast to the normal "DropObject(obj)" that is
usually called. Programmers of METH_INIT take note! The
METH_DESTROY is only called when the usecount falls to zero.
However, if it is zero to begin with (as, say, it is when it
returns from the METH_CREATE method), you must first increment
the usecount, and -then- decrement it.
It is the METH_INIT that follows which is responsible for setting
up the usecount to properly be non-zero. And, of course, to setup
the object correctly.
The CreateSubClass is a simpler wrapper that ends up calling the
METH_SUB method of a particular class. The "privateClass" pointer
lookup is exactly the same in the CreateSubClass() as in the
CreateInstance(); however, it is the METH_SUB method that actually
invokes the METH_CREATE and the METH_INIT, not CreateSubClass()....
Wrap-up of Methods:
There are many ways to call methods besides DoShadow(). We have seen
at least two examples -- DoShadowAsync() and PreParseShadow(). The
alternatives are documented in the ShadowLibFuncs.doc. In
addition, the root SHADOW library code for all of these functions is
DSM(), and that is documented in ShadowLibraryFuncs.doc (autodoc). I
highly recommend using these references to learn more about the
method invocation code. We are about to go on to the more advanced
topics of PATCHER_CLASS, PROCESS_CLASS and DIRECTOR_CLASS which will
introduce even more complexity into an already complex picture, this
is a convenient break point to try some example code and see what
happens (let me know of any bugs!). It will allow you to get a feel
for the programming environment that SHADOW lives within and will
make you feel more comfortable about the next three topics.
PATCHER CLASS
You will notice that in the above discussion about methods there were
several METH_FLAGS_* that were never covered and left later for the
discussion about PATCHER_CLASS. Well, the time has come.
Yes, that's right, just as you thought you were beginning to
understand everything about SHADOW's methods, some new twist pops up
to confuse the issue. PATCHER_CLASS is a rather good example of
this.
Essentially, all methods within all SHADOW classes are patchable.
Patching is analogous to SetFunction(), except that great pains have
been taken to assure that the problems with SetFunction() are NOT
shared by PATCHER_CLASS. Unfortunately, you also lose some of the
flexibility of SetFunction().
When a method is patched, the result is referred to as a
"patch-chain." DSM(), along with all of its other duties, is
responsible for traversing the patch-chain and calling each of the
separate method patches in turn and returning the most-recent, valid
return value.
Each patch is created in very nearly the same manner that the base
methods were created -- through the use of the METHOD_TAG structure.
Note, however, that each patch will require ONLY ONE METHOD_TAG
item -- please do not attempt to pass single METHOD_TAG *items* to
functions like SetupMethodTags() which requires a METHOD_TAG *array*.
This will not work, so you'll have to setup the mtag_procObject and
mtag_defnObject fields by hand (or put all of your METHOD_TAG patches
in one big array and call SetupMethodTags() on this array.
There are two significant differences between the METHOD_TAGs that
are used for the base methods and the METHOD_TAGs that are used for
patches. The former ignores the mtag_priority field while the
latter uses it to order the patch-chain. The patches in the patch
chain are called in descending numerical order where the base
method is always assumed to exist at priority zero.
The second difference is the additional mtag_flags values which,
while supported by the base methods, gain real power only when
patch-chains exist:
METH_FLAG_PRE_BLOCK
METH_FLAG_POST_BLOCK
METH_FLAG_CHECK_CONTINUE
METH_FLAG_NO_RTRN
METH_FLAG_PRE_BLOCK:
If this flag is set, DSM() will stop invoking the patches within the
patch-chain. Additionally, the patch/base-method itself will not
get invoked. This is useful if you want to interrupt your
patch-chain during the debugging phase, or if you want to disable
some of the features in a demo version -- simply add the PRE_BLOCK to
the base methods' mtag_flags, or add a PRE_BLOCK'd patch at some
relatively high priority to the class to be affected.
METH_FLAG_POST_BLOCK:
If this flag is set, DSM() will stop invoking the patches within the
patch-chain, but only after invoking this patch/base-method. This is
useful for replacing a base-method during a bug-release update. Add
a POST_BLOCK'd patch to the base-method at a priority greater than
zero and the base-method is effectively replaced by the patch.
METH_FLAG_CHECK_CONTINUE:
If this flag is set, the patch returns an additional value in d1 which
informs DSM() whether or not to continue the patch. This allows you
to dynamically change whether your patch executes, or the base-method
executes. If d1 is returned as zero from the patch, then the
patch-chain invocation ends and the latest, valid return value is
returned. Otherwise, the patch-chain continues as it would have.
METH_FLAG_NO_RTRN:
This informs DSM() that your patch or method does not return a valid
value, so any previous return values that have accumulated while
traversing the patch-chain should remain intact. Note that this is
a significant inversion of meaning from SHADOW 4's
METHOD_FLAG_RTRN_ME.
Several important notes should be made at this point. First, the
traversal through the patch chain is controlled by a semaphore on the
patch-chain's list. This means that if you attempt to do something
silly like have a base-method which patches itself, or a patch/base-
method which attempts to remove a of the patch, the semaphore will
nicely lock up your code. Secondly, the traversal through the
patch chain is guaranteed to be serial. This means that even
if you call DoShadowAsync(), the method will actually be preparsed,
sent to the base-method's mtag_procObject asynchronously, and only
then will the patch-chain invocation will occur. This allows
behaviour which depends on serial traversal (like the
FLAG_CHECK_CONTINUE) to continue to operate correctly, even though
each patch is actually invoked asynchronously to the invoker.
Third, while patches are invoked from high priority to low priority,
the actual return value will be the return value from the -lowest-
priority patch that ended up returning a valid return value. Fourth,
the method that is being patched must ALREADY exist within the class
that's being patched -- the only way to add new methods is to modify
the superclass hierarchy -- this will be covered at the end of the
PATCHER_CLASS description. Fifth, there is a speed penalty to using
patch-chains -- do not expect them to ever be very speedy. And last,
but certainly not least, because DSM() controls the patch-chain
traversal, there is no way to modify the arguments as they travel
from patch to patch. Please be careful and act accordingly.
The following code demonstrates the PATCHER_CLASS in action:
/*
* The patches
*/
extern double PreTestMethod(METHOD_ARGS);
extern long PostTestMethod(METHOD_ARGS);
METHOD_TAG preMethod =
{
"Method TEST",
NULL, NULL,
INVOKE_SYNC,
METH_FLAG_CHECK_CONTINUE |
METH_FLAG_CLASS |
METH_FLAG_NO_RTRN,
1, /* The priority */
(METHODFUNCTYPE)PreTestMethod,
NULL
};
METHOD_TAG postMethod =
{
"Method TEST",
NULL, NULL,
INVOKE_SYNC,
METH_FLAG_OBJECT |
METH_FLAG_NO_RTRN,
-1,
(METHODFUNCTYPE)PostTestMethod,
NULL
};
/*
* Add pre and post patches to the dosClass.
* Note that the "Method TEST" method MUST ALREADY exist
* in the dosClass definition.
*/
preMethod.mtag_procObject = dosTaskClass;
preMethod.mtag_defnObject = CurrentProcess();
preObject = CreateInstance(NULL, PATCHER_CLASS, META_CLASS,
&preMethod,
dosClass,
METHOD_END
postMethod.mtag_procObject = dosTask;
postMethod.mtag_defnObject = CurrentProcess();
postObject = CreateInstance(NULL, PATCHER_CLASS, META_CLASS,
&postMethod,
dosClass,
METHOD_END);
/*
* The method has now been patched!
* Now, when you call the method, the preMethod is invoked in an
* instantiated dosTaskClass-process; then the regular method is
* invoked; then the postMethod is invoked synchronously in the
* dosTask process.
*/
.
.
.
/*
* Remove the patches.
* You could have added this to the process with the
* AddAutoResource() call, in which case you would not
* need to call RemoveObject()....
*/
RemoveObject(preObject);
RemoveObject(postObject);
/*
* The patcher methods.
*
* The PreTestMethod() uses a SAS/C 5.0 side-effect where
* a double is returned in d0 and d1.
* Whether the patch-chain continues or not alternates
* between TRUE and FALSE.
*/
double PreTestMethod(METHOD_ARGS)
{
static int __far myLocalVar = 0; /* YES -- __far is
required! */
union {
double tempD;
ULONG tempV[2];
} retval;
BPTR oldoutput;
oldoutput = SelectOutput(Open("CONSOLE:", MODE_OLDFILE));
VPrintf("Pretest called in <%s> task.\n",
(ULONG *)&(FindTask(NULL)->tc_Node.ln_Name));
if (!(retval.tempV[1] = (myLocalVar++ & 1)))
VPrintf("PreTest will block this call:\n", NULL);
oldoutput = SelectOutput(oldoutput);
Close(oldoutput);
retval.tempV[0] = 0; /* This number is NOT a valid return!
* Note the METH_FLAG_NO_RTRN
*/
return retval.tempD;
}
/*
* Note that the "return 200" is NOT a valid return as the
* METHOD_TAG has the METH_FLAG_NO_RTRN flag set.
*
* As the preMethod and the postMethod never return a
* valid return value, the return value of the base-method
* is returned, -if- the preMethod doesn't return a FALSE (which
* would end the patch-chain, and therefore prevent the base-
* method from being called in the first place. In that case,
* the DSM() [DoShadow(), et. al.] returns zero.
*/
long PostTestMethod(METHOD_ARGS)
{
VPrintf("Post test called\n", NULL);
return 200;
}
While PATCHER_CLASS allows you to patch existing methods, it does not
allow you to add additional methods. There is a way to add methods
to a class, unfortunately, there is no way to later -remove- those
methods. The way in which this occurs is to change the superclass
hierarchy so that the class that you want the method to be added to
is changed to be a subclass of a class that is created with the
method you desire.
In otherwords, if you wish to add a method to the PROCESS_CLASS,
you create an intermediate class with the additional method, then
point the superclass of the PROCESS_CLASS to the intermediate class.
It is, of course, required that the intermediate class be a subclass
of the superclass of the "PROCESS_CLASS", or, the class your adding
the method to. You can use the SHADOW library function call --
InsertSuperClass(), or the METH_SUPER method call as demonstrated
below:
/*
* Creates a new superclass of a subclass.
* Adds the methods specific in the my_new_method structure.
*/
procClass = FindShadowClass(PROCESS_CLASS, META_CLASS);
newClass = DoShadow(procClass, NULL, METH_SUPER,
MY_INTERMEDIATE_CLASS,
NULL,
my_new_attributes,
my_new_methods,
METHOD_END);
DropObject(procClass);
.
.
.
RemoveObject(newClass);
This example is taken directly from the ShadowLibraryMethods.doc,
where you can find more information about the METH_SUPER method. In
addition, you can find out more about adding new methods by reading
the InsertSuperClass() method.
Obviously, the preferred manner of adding methods to classes is to
-subclass- them and create your own classes. However, PATCHER_CLASS
exists to provide as much flexibility and extensibility as possible.
DIRECTOR CLASS
DIRECTOR_CLASS expands upon the notion of extensibility by providing
notification for important system and program state changes. For
instance, you can receive notification whenever a class is added
or removed from the system list, or when a -particular- class is
added or removed from the system. In the gui I've been
experimenting with, the ATTR_GUICHILDREN attribute which is a
"watched" binary tree that contains a GUI-object's children,
generates notification whenever any children are removed or
added to the system.
In addition to being able to watch a particular GUI-object, you can
watch all the GUI-object's of any particular class (and associated
subclasses) that you'd like. It is entirely feasible to imagine
receiving notification whenver any program added any GUI-object to
the child tree of any other GUI-object.
DIRECTOR_CLASS objects handle the request for notification of these
system and program state changes. These state changes occur in
special kinds of "state" or "watched" variables. You can create a
WatchedVariable in the following manner:
void *nullPtr = NULL;
struct WatchedVariable wv = {NULL, &nullPtr, initial_value};
The &wv can then be used when establishing a connection in
the DIRECTOR_CLASS. However, as SHADOW is mainly an object-
oriented system (or, rather, it seems to have grown into being one),
it is more frequent to find yourself establishing a director-object
connection to a watched variable inside of a class.
A watched variable inside of a class is created in a very special
way that allows you not only to watch a single object, but all the
objects that are instances of a particular class (and instances of
all classes that are subclasses of that particular class).
Here is an example attribute definition:
ATTRIBUTE_TAG guiAttrs[] =
{
{
ATTR_GUICHILDREN, FLAG_ATTR_WATCHED |
SHADOW_TREE,
NULL
},
TAG_END
}
This attribute creates a watched variable that allows you to use
the attribute as an AVL tree. Although, instead of calling the
regular AddTreeNode() and/or RemoveTreeStringNode(), you call the
AddWatchedTreeNode() and/or RemoveWatchedTreeStringNode(). As a
director watching this attribute, you can get notification about
when a node is added or removed from the tree (you can also
receive notification when some other director establishes or
terminates a connection to any watched variable, but that gets
just a little ridiculous...).
There are three basic types of watched variables. One is the
long-word type, another is the AVL tree type which is shown
above, and the last is the singly-linked list type which I
won't bother to cover in the examples. These three types
are denoted by three different flags located in <shadow/watcher.h>:
SHADOW_VALUE
SHADOW_TREE
SHADOW_LIST
Use these flags in the ATTRIBUTE_TAG value when creating a
watched variable in an object.
Watching a watched variable is a two-step process. First you
need to create an instance of a director class. There are several
things to setup at this phase -- the name of the director
object, the object that will receive the notification and the
selector that will be invoked on that object, the type of events
you want to receive notification about, the type of watcher, and
the default resource tracking cleanup algorithms that will be
used when the director's connections are terminated and the
director is removed from the system.
Second, you need to extablish a connection with a particular watched
variable/attribute or class of watched attributes. Unlike method
patches which are can only be installed in a single class at a time,
director objects can establish themselves in any number of watched
variables -- allowing you to save, perhaps, a bit of memory in the
process.... The ESTABLISH method only takes arguments which
defines the watched variable and returns a handle to the connection
which should either be sent to DropObject(), or used in any
explicit call to the METH_DIRECTOR_TERMINATE method.
Setting up a director object is less complex than method patches, but
nevertheless there are a few flags you should know about [the
following flags are located in <shadow/watcher.h>]:
/*
* Match either.
*/
W_CHANGE_ZERO
W_CHANGE_NON_ZERO
W_CHANGE_VALUE
/*
* Match exactly
*/
W_NODE
W_WATCH_CHANGE
W_REMOVE
W_INSERT
/*
* Control matching.
*/
W_MATCH
W_MATCH_VALUE W_MATCH
W_SECOND
W_MATCH_FIRST W_MATCH
W_MATCH_SECOND (W_SECOND | W_MATCH)
W_INSERT_NODE (W_NODE | W_INSERT)
W_REMOVE_NODE (W_NODE | W_REMOVE)
W_INSERT_WATCHER (W_WATCH_CHANGE | W_INSERT)
/* Not valid during... */
W_REMOVE_WATCHER (W_WATCH_CHANGE | W_REMOVE)
/* ... INIT[]/DESTROY[] */
The exact meaning of all of those flags is covered in detail under
the METH_DIRECTOR_NOTIFY method description in ShadowMethods.doc.
In general, these flags describe which state-change events the
director object is interested in getting notification about.
/*
* for use in passing to METH_INIT....
*/
W_FLAG_AUTOBREAK
W_FLAG_AUTOREMOVE
Do -not- confuse these with the W_AUTOBREAK and W_AUTOREMOVE flags
which are NOT to be passed into the METH_INIT function!
The W_FLAG_AUTOBREAK informs the director object that when the
director is sent a METH_REMOVE method, it should terminate all of
its outstanding connections.
The W_FLAG_AUTOREMOVE informs the director object that when the
last connection is terminated, the director should automatically
be removed from the system.
By default, I suggest you use both flags unless you think of some
-very- good reason not to.
W_OBJECT
W_CLASS
These two flags specify which type of director you wish to create --
one to watch an entire class, or one to watch just a specific
object or watched variable.
/*
* As flag to TERMINATE METHOD.
*/
W_HANDLE
W_SORT
There are three ways to TERMINATE a connection. The first is to
create the director with the W_FLAG_AUTOBREAK flag set and then
send a METH_REMOVE method (ie. RemoveObject()) to the director.
This will terminate all connections. The second is to specify,
using the handle returned to you by the ESTABLISH call, a specific
connection be TERMINATEd. This is the W_HANDLE case, and if left
unspecified, the default manner is W_HANDLE. The third case is to
specify an object that the director was watching and TERMINATE any
connection in that object -- however, you are not guaranteed
-which- connection is terminated, if you are watching more than one
attribute, or one attribute more than once, or some combination
thereof. This third possibility is usually used by the system,
rather than the external programmer -- there are very arcane
reasons for why this needs to exist....
There are two different types of directors -- class-watchers and
object-watchers. Herein we demonstrate both:
/*
* Create a director object.
* Specify the director's name as "WatchWindows".
* Specify the notification be invoked on "object" with the
* METH_TELL_ME method.
* Specify interest in addition or removal of nodes from a
* complex watched variable like SHADOW_TREE or SHADOW_LIST
* Specify a class watcher
* Specify the autoremove and autobreak features.
*/
GlobalDirector = CreateInstance(NULL,
DIRECTOR_CLASS,
META_CLASS,
"WatchWindows",
object,
METH_TELL_ME,
W_INSERT_NODE | W_REMOVE |
W_CLASS |
W_FLAG_AUTOBREAK |
W_FLAG_AUTOREMOVE,
METHOD_END);
/*
* Let's watch for additions and removal of children to any
* object of the WINDOW_CLASS, or any instance of any
* subclass of the WINDOW_CLASS.... The watched-tree
* attribute that contains the windows' children is
* called ATTR_GUICHILDREN. It is not specific to just the
* WINDOW_CLASS, though....
*
* Note that we throw away the handle that the ESTABLISH
* method returns.
*/
windowClass = FindShadowClass(WINDOW_CLASS);
DropObject(DoShadow(GlobalDirector,
NULL,
METH_DIRECTOR_ESTABLISH,
ATTR_GUICHILDREN,
windowClass,
METHOD_END));
DropObject(windowClass);
.
.
.
/*
* Cleanup!
* We didnt't add the GlobalDirector to our AutoResource() tree,
* so we'll have to do an explicit RemoveObject()!
* Remember, the W_FLAG_AUTOBREAK means that the METH_REMOVE
* terminates all connections.
*/
RemoveObject(GlobalDirector);
The following example will demonstrate a director watching an object,
or watching a plain watched variable that does not exist within an
object:
/*
* Create a director object.
* Specify the director's name as "Watch Meta".
* Specify the notification be invoked on "object" with the
* METH_INFORM_ME method.
* Specify interest in addition or removal of nodes from a
* complex watched variable like SHADOW_TREE or SHADOW_LIST
* Specify an object/variable watcher
* Specify the autoremove and autobreak features.
*/
mlo->director = CreateInstance(NULL,
DIRECTOR_CLASS,
META_CLASS,
"Watch Meta",
object,
METH_INFORM_ME,
W_INSERT_NODE | W_REMOVE |
W_OBJECT << 16 |
W_FLAG_AUTOBREAK |
W_FLAG_AUTOREMOVE,
METHOD_END);
/*
* If there is a specific 'meta' to watch, let's watch
* for additions or removals of instances to/from the meta's
* ATTR_INSTANCETREE. If there is no meta (that is, if meta
* is NULL), let's watch SHADOW's public Watched Variable
* which contains all the current metas and watch for a
* meta to be added or removed.
*
* Note that we throw away the handle that the ESTABLISH
* method returns.
*
* Note, we pass in the object and the object's attribute
* that we want notification about (meta dn ATTR_INSTANCETREE
* in this case. If the object is NULL, the attribute, which
* is usually a string, is now assume to point to a watched
* variable structure instead.
*
* Yes, we really want a W_OBJECT watcher to watch a meta.
* Why? Because we are watching -one- meta, not a whole
* *class* of metas, which is where we would want to use a
* W_CLASS type of director. In otherwords, if you wanted to
* watch the ATTR_INSTANCETREE of a whole class of metas,
* you'd use a W_CLASS. Here, though, we need a W_OBJECT
* director object.
*/
DropObject( DoShadow(mlo->director,
NULL,
METH_DIRECTOR_ESTABLISH,
(meta)?ATTR_INSTANCETREE:
&ShadowBase->sb_metaTree,
meta,
METHOD_END));
.
.
.
/*
* Cleanup!
* We didnt't add the mlo->director to our AutoResource() tree,
* so we'll have to do an explicit RemoveObject()!
* Remember, the W_FLAG_AUTOBREAK means that the METH_REMOVE
* terminates all connections.
*/
RemoveObject(mlo->director);
For this example, we explore explicit TERMINATion of a director's
connections.
/*
* Create a director object.
* Specify the director's name as "WatchBlockedChildren".
* Specify the notification be invoked on "object" with the
* METH_SLAP_ME_ONCE method.
* Specify interest in addition or removal of nodes from a
* complex watched variable like SHADOW_TREE or SHADOW_LIST
* Specify an object/variable watcher
* Specify the autoremove and autobreak features.
*/
director = CreateInstance(NULL,
DIRECTOR_CLASS,
META_CLASS,
"WatchBlockedChildren",
object,
METH_SLAP_ME_ONCE
W_INSERT_NODE | W_REMOVE |
W_OBJECT << 16 |
W_FLAG_AUTOBREAK |
W_FLAG_AUTOREMOVE,
METHOD_END);
/*
* Establish the connection for the director.
* Here, we'll watch the ATTR_GUICHILDREN of an object.
*/
handle1 = DoShadow(director, NULL, METH_DIRECTOR_ESTABLISH,
ATTR_GUICHILDREN,
object,
METHOD_END);
/*
* Establish another connection.
* Here, we'll watch the ATTR_INSTANCETREE of WINDOW_CLASS.
* Note that we aren't interested in the INSTANCE_TREE
* attribute for -all- classes, just for the on particular
* -class-instance- -- WINDOW_CLASS, which is why we want
* a W_OBJECT watcher as opposed to a W_CLASS watcher.
*/
windows = FindShadowClass(WINDOW_CLASS);
handle2 = DoShadow(director, NULL, METH_DIRECTOR_ESTABLISH,
ATTR_INSTANCETREE,
windows,
METHOD_END);
DropObject(windows);
.
.
.
/*
* Terminate the first connection.
*/
DoShadow(director, NULL, METH_DIRECTOR_TERMINATE, handle1,
METHOD_END);
.
.
.
/*
* Terminate the second connection.
*/
DoShadow(director, NULL, METH_DIRECTOR_TERMINATE, handle2,
METHOD_END);
.
.
.
/*
* Cleanup!
*
* Note, because we specified the W_AUTOREMOVE feature, and
* because we've closed all of the connections, the director
* has already been removed. Therefore, simply DropObject()
* it.
*/
DropObject(director);
This code is taken directly from the Browser included in the SHADOW
distribution. Browser uses W_OBJECT watchers to maintain a current
list of the available classes, objects, and number of patches on a
method. If you add a class while the Browser is showing a list of
the classes, that list is updated automatically, without the active
knowledge of the program creating the class!
PROCESS CLASS
Process classes offer several hurdles that are not encountered
with any other objects. First, the initialization of a process is
primarily a two-stage procedure -- the creation of an AmigaDOS
process, and then the initialization of all of the ports, libraries,
etc. that that particular task requires. Note that the second
stage -must- occur within the created task, whereas the first
stage -cannot- occur within that task (it wouldn't be around at
that point). Second, the cleanup of processes (ie: the removal
of ports and libraries bases, etc.) must occur within the task
itself, but cannot actually occur until all references to the
process object are eliminated. On the otherhand, many of the
resources added to a process via the AddAutoResource() call
reference the process object (usually indirectly, through
an mtag_defnObject field in a method within a class, for instance).
Therefore, the METH_REMOVE must clear all of the automatic
resources, but the METH_DESTROY is responsible for actually
closing down the ports, allocated signals, and library
bases.
Additionally, process creation must take into account that a program
launched via the CLI or the Workbench does not have a SHADOW
process object associated with it, and therefore does not contain
the requisite ports and messages to deal with the sending and
receiving of SHADOW methods. There must be a way to associate
an already existing process with a SHADOW process object after
the process creation (note how similar this requirement is to
the two-stage startup procedure described in the above paragraph).
Finaly, processes should provide an example on how to properly deal
with SHADOW messages by providing a default message handler which
handles all of the various SHADOW methods and returns when the
process gets a ^C (a ^C is automatically sent to the process by
the system under certain shut-down conditions as well).
Process and Program Startup and initialization
The initialization of a process object is separated into two separate
parts -- a METH_INIT and a METH_PROC_ASSOCIATE. The METH_INIT
takes several parameters:
The name of the process
One of two parameters that prevents a parent process from
exitting before its children
A taglist that is passed to the CreateNewPoc() AmigaDOS call
A ProcessInitFrame -- for the METH_PROC_ASSOCIATE call
A ProcessHandlerFrame -- for the METH_PROC_HANDLER call
Let's take them one at a time, ignoring the process name argument,
as its function is obvious.
Let us say that you start a thread on one of your processes. You
definitely do not want your parent process going away before your
child threads have all disappeared -- there are two ways to prevent
this. First, you can pass an Exec semaphore pointer to the
INIT code. The thread that is created will grab a SHARED lock on
this semaphore between the time that the METH_INIT is called,
and the time when the METH_INIT returns (the magic involved therein
is unimportant).
Therefore, before your parent process disappears, it should attempt
to grab an exclusive lock on this semaphore. You can accomplish
this by passing the semaphore into the RemoveCurrentProgram()
call that all SHADOW programs call in their cleanup code.
A much easier way (and the preferred SHADOW V way to deal with these
issues) is to pass the parent's own process object into the the
METH_INIT call. SHADOW will then resource track the parent process
within the child thread. The parent process will be dropped only
after the program returns from the _HANDLER routine, thereby
preventing the parent process from disappearing before its children.
The taglist is a taglist that is passed to the CreateNewProc()
library call under AmigaDOS. Several tags are reserved for
exclusive use by SHADOW. They are:
NP_Name
NP_ExitData
NP_Entry
NP_Name is set to the name passed into the INIT call.
NP_ExitData is used internally.
NP_Entry is used internally. You should use the ProcessHandlerFrame
to set your entrypoint -- this will be a -method- entry-point....
The ProcessInitFrame is a METHOD_END-terminated array of the
arguments that will be used to finish the creation of the process.
If NULL, the internal definition of the arguments that
METH_PROC_ASSOCIATE takes is used. You may override which method is
called to complete the process initialization by specifying a non-
NULL field for the pif_methodID field. If NULL, the method is
assumed to be METH_PROC_ASSOCIATE. In addition, you may add any
number of arguments to the end of the InitFrame, though the
final arguments should all be taken by the destination method.
The following is the internal ProcessInitFrame that is used by
SHADOW. It resides in <shadow/process.h>:
struct ProcessInitFrame {
struct CoreObject *pif_object;
struct Meta *pif_class;
char *pif_methodID;
char *pif_procName;
void *pif_args[1]; /*
* Other arguments,
* terminated by METHOD_END
*/
};
The METH_INIT code fills in the object, and the class, and changes the
methodID as required. If the ProcessInitFrame is passed in, the
pif_procName is not touched, and the rest of the arguments are
assumed to be valid and terminated by a METHOD_END. The entire
array is passed to DSM() which returns a message encapsulating
the appropriate method call. The actual invocation of this
method will occur once the process starts up.
The ProcessHandlerFrame is the entry point (method) into the thread.
By default, this is the METH_PROC_HANDLER method which takes no
arguments. As with the ProcessInitFrame structure, the actual
method can be overrided by passing in your own methodID in the
ProcessHandlerFrame. A NULL methodID field in a passed
ProcessHandlerFrame is assumed to mean METH_PROC_HANDLER.
The following is the internal ProcessHandlerFrame that is used by
SHADOW. It also resides in <shadow/process.h>:
struct ProcessHandlerFrame {
struct CoreObject *phf_object;
struct Meta *phf_class;
char *phf_methodID;
void *phf_args[1]; /*
* Other arguments,
* terminated by METHOD_END
*/
};
Once again, either the default ProcessHandlerFrame, or one that you
provide, is passed into DSM() and a message is returned. During
the internal startup code of the thread, the ProcessHandlerFrame's
message is invoked subsequent to the ProcessInitFrame's message.
Once both of these messages are created, the actual AmigaDOS process
is created. The METH_INIT will not return yet, though, the
METH_PROC_ASSOCIATE (or the method specified in the ProcessInitFrame)
is allowed to run first. If this method returns 0L, then the
initialization is assumed to have failed, the thread shuts down, and
the METH_INIT returns a NULL process object. If this method
returns a non-zero value, the parent process is signalled to
wakeup, and the thread calls the METH_PROC_HANDLER method (or
the method specified in the ProcessHandlerFrame).
When the parent process gets woken up, it attempts to discover
whether or not the process ended up working or not, and if it did,
the process object is returned, otherwise NULL is returned. In
addition, if your process is created, the tag entries are
TAG_IGNORE'd -- this way you know whether you need to free any
resources the tags might have pointed to (like file pointers for
stdin and stdout). Note that these fields are cleared whether or not
the ProcessInitFrame method succeeded, the important point is
whether or not the process was created. After all, the process
cleanup code that DOS runs cleans up stdin/out, etc....
Much has been said about METH_PROC_ASSOCIATE -- that there is a
method which is called (by default) by the new thread when the
process is created, but very little has been said as to what it
actually is supposed to do, and, no mention of hooking-up already
existing processes with SHADOW process objects has been made.
The METH_PROC_ASSOCIATE is the second stage of the two-stage process-
creation procedure. It is responsible for allocating ports for the
new process object, opening up any libraries you might want opened,
creating any additional ports (like an IDCMP port, for example)
that you might want -- generally, any resource allocation that a
process has to do up-front. In addition, once all of this is
successfully done, the METH_INIT method is forwarded from the
PROCESS_CLASS to the ROOT_CLASS. The process object, therefore, is
not on any system lists until the METH_PROC_ASSOCIATE is called.
Also, for your information, the process object is stored in the
tc_UserData field of the process -- MAKE SURE YOU DON'T TOUCH IT!
In addition, when a program starts, you are already given a process,
but no SHADOW object exists for this process. Creating a process
object and calling METH_PROC_ASSOCIATE on it allows you to attach
the AmigaDOS process to a SHADOW object, even after AmigaDOS has
actually created the process.
Startup in your program might look like this:
ShadowBase = OpenLibrary("shadow.library", 5);
if (!ShadowBase)
abort_and_cleanse();
procClass = FindShadowClass(PROCESS_CLASS);
procObject = DoShadow(procClass, NULL, METH_CREATE, METHOD_END);
DropObject(procClass);
/*
* an object has been created, now initialize it!
* Note: procObject is swallowed by this call -- consider it
* never to have really existed as an object....
*/
if (!DoShadow(procObject, NULL, METH_PROC_ASSOCIATE,
"My program",
METHOD_END))
abort_and_cleanse();
/*
* Okay, our process is running!
*/
While this is useful if you want to start with something other than
a PROCESS_CLASS object, the default PROCESS_CLASS is very useful, and
a library call can be used instead of this code, which does
essentially the same thing:
ShadowBase = OpenLibrary("shadow.library", 5);
if (!ShadowBase)
abort_and_cleanse();
if (!InitOOProgram("My program"))
abort_and_cleanse();
Note that we do NOT call CreateInstance() for our process object.
That, of course, is because we do not want a -new- process, just a
new SHADOW object to attach to an already existing process....
Along with METH_PROC_ASSOCIATE, you've also come across
METH_PROC_HANDLER. As a default, this is merely the main event loop.
The main event loop returns when a ^C is sent to the process, however
messages may continue to get handled even after the ^C is sent so
that the process can get a shot at deleting its resources....
Consider, then, the following complex example:
Process A's handler first sends an asynchronous method to
process A to, say, draw a random circle. It then invokes the
superclass METH_PROC_HANDLER for default message handling.
Within the draw circle routine is a method invocation that,
again, invokes an asynchronous method to draw another random
circle. Obviously, this executes ad-nauseum. Now, the
default handler code is careful about buffering the message
lists, and therefore allowing the ^C to get processed to start
the quit sequence of the program, however, because the program
always has a method waiting for it, it can never shutdown.
Therefore, your custom handler should also set a global
variable when the system METH_PROC_HANDLER returns, and the
circle drawing should note this flag and NOT send another
asynchronous method when it is sent....
Fortunately, however, SHADOW already provides such a flag.
In particular, SHADOW_PROC_FLAGS_REMOVED in ATTR_SHADOWPROCESS'
sp_flags field is set when the process has returned from the
HANDLER (or ParseHandlerFrame) routine. Note that this is not
set within the METH_PROC_HANDLER method, but is set after the
routine has exitted by the internal processStartup code.... Thus,
if you subclass the METH_PROC_HANDLER routine, the flag may or may
not be set when the superclass' METH_PROC_HANDLER method returns to
you. Indeed, as noted, you may even be overriding which method
call is actually made (via the ParseHandlerFrame). It is when this
method returns that the flag is actually set.
Two more examples for initialization:
child = CreateInstance(NULL, PROCESS_CLASS,
META_CLASS,
METH_INIT,
"New Process",
CurrentProcess(),
/* Parent process object */
NULL,
/* A NULL semaphore --
strictly speaking, you could
have gotten away with putting
the METHOD_END here, it is
included to remind you that
the parent object is separate from
the semaphore field */
METHOD_END);
/*
* Yep! A new process has started up (assuming child not NULL).
* We may now use this child in a call to SetupMethodTags() to
* allow some of our methods to run in the child process.
*/
success = AddAutoResource(NULL, child, "New Process");
DropObject(child)
.
.
.
/*
* cleanup!
*/
RemoveCurrentProgram(NULL);
CloseLibrary(ShadowBase);
.
.
.
This example shows off how to use the ParseFrames correctly:
Let's say that you have written a METH_PROC_HANDLER for your
program's processes in the following fashion:
ARGUMENT_TAG REF_HandleMessages[] =
{
ARG_STR,
ARG_TAGL(sizeof(struct MethodTag)),
ARG_TAGL(sizeof(struct AttributeTag)),
RET_NORMAL
};
void HandleProcMessages(METHOD_ARGS, char *baseClassName,
char *newModuleClassName,
struct MethodTag *methods,
struct AttributeTag *attrs)
{
geta4(); /* Yep, don't forget this! */
/*
* Allocate a module class for this process.
*/
AddAutoResource(NULL, CreateSubClass(NULL,
baseClassName,
META_CLASS,
newModuleClassName,
NULL,
methods,
attrs,
METHOD_END),
newModuleClassName);
DoSuperShadow(object, class, MethodID, METHOD_END);
}
Now, for your own program's process startup, you would naturally use
something like the following:
/*
* Create the process object and attach to the program.
*/
procClass = FindShadow(MY_PROCESS_CLASS);
success = DoShadow( DoShadow(procClass,
NULL,
METH_CREATE,
METHOD_END),
NULL,
METH_PROC_ASSOCIATE,
NEW_PROCESS_NAME,
METHOD_END);
DropObject(procClass);
if (success)
{
/*
* Startup the process and handle all incoming
* methods. Also, create my module's class.
*/
DoShadow(CurrentProcess(), NULL, METH_PROC_HANDLER,
BASE_MODULE_CLASS,
MY_NEW_MODULE_CLASS,
myMethods,
myAttrs,
METHOD_END);
RemoveCurrentProgram(NULL);
}
/*
* Get out!
*/
CloseLibrary("shadow.library", 5);
exit();
However, this does not solve a bigger problem -- how to create -new-
processes that are not related to an already existing program-process.
Obviously, the normal METH_INIT will not work, as the normal
METH_PROC_HANDLER is called without any arguments. We can do one of
two things. The first crack is to call the METH_INIT in the following
clumsy fashion:
struct MyHandlerFrame {
OBJECT object;
META class;
char *MethodID;
char *baseClass;
char *newClass;
METHOD_TAG *methods;
ATTRIBUTE_TAG *attrs;
void *end;
} mhf;
mhf.MethodID = NULL;
mhf.baseClass = BASE_MODULE_CLASS;
mhf.newClass = MY_NEW_MODULE_CLASS_2;
mhf.methods = myMethods_2;
mhf.attrs = myAttrs_2;
mhf.end = METHOD_END;
child = CreateInstance(NULL,
MY_PROCESS_CLASS,
META_CLASS,
METH_INIT,
"New Process #2",
CurrentProcess(),/* Parent process */
NULL, /* No semaphore */
NULL, /* no tags */
NULL, /* No InitFrame */
&mhf
METHOD_END);
This is clumsy because each time you want to create a new process,
you also have to setup the MyHandlerFrame. In addition, because
the MyHandlerFrame structure has no definitive -length- to it, the
METH_INIT cannot be called asynchronously. A much better way is to
redefine the way you call the METH_INIT for the process as follows:
/*
* ProcessClass' METH_INIT
*
* NULL if error, else returns the new class-object.
*
*/
struct ArgumentTag REF_InitProcObject[] = {
ARG_STR,
ARG_STR,
ARG_STR,
ARG_TAGL(sizeof(struct MethodTag)),
ARG_TAGL(sizeof(struct AttributeTag)),
RET_OBJ
};
OBJECT InitProcObject(METHOD_ARGS, char *processName,
char *baseClassName,
char *newModuleClassName,
struct MethodTag *methods,
struct AttributeTag *attrs)
{
struct MyHandlerFrame {
OBJECT object;
META class;
char *MethodID;
char *baseClass;
char *newClass;
METHOD_TAG *methods;
ATTRIBUTE_TAG *attrs;
void *end;
} mhf;
mhf.MethodID = NULL; /* default! */
mhf.baseClass = baseClassName;
mhf.newClass = newModuleClassName;
mhf.methods = methods;
mhf.attrs = attrs;
mhf.end = METHOD_END;
/*
* This method always runs as a callback!
* So we don't have to worry about resource tracking
* the variable length HandlerFrame.
*/
return DoSuperShadow(object, class, MethodID,
processName,
CurrentProcess(),
NULL, NULL, NULL,
&mhf,
METHOD_END);
}
This is much cleaner, as now to create a new process, all you need to
do is the following:
child = CreateInstance(NULL, META_CLASS, MY_PROCESS_CLASS,
"New Process #3",
BASE_MODULE_CLASS,
MY_NEW_MODULE_CLASS_3,
myMethods_3,
myAttrs_3,
METHOD_END);
This, obviously, looks a lot better, it hides the common code
in one place, allowing you to easily create new modules that use the
same master process code without duplicating all of the
initialization/structure setups....
Process and Program Shutdown:
The destruction of a process has similar problems as the
initialization of processes. Remember that the destruction of an
object -already- occurs in two stages -- the REMOVE and then the
DESTROY. Unfortunately, the METH_DESTROY method really needs to be
called as a function, there is, therefore, no guaranteed way to get
the METH_DESTROY to occur within the process. The destroy process,
therefore, has been broken up into two pieces:
METH_DESTROY -- this sets a DESTROY bit in the ShadowProcess
structure and then signals the task with a
^C
METH_PROC_DISASSOCIATE -- this destroys the ports that were
allocated by SHADOW, and you should
subclass this to destroy/close and
resources you created/opened in the
ASSOCIATE method. When this method
returns the process' name has also
been freed.... The object's
destruction is completed with magic
that will not be discussed here, but
it is -not- done by the
METH_DISASSOCIATE method.
Neither of these methods should be called by you, neither do they
take any arguments. You may, however, subclass either one of
them. For the most part, anything that you would have normally
put into the the METH_DESTROY you should put into the
METH_DISASSOCIATE instead....
By the way, both of these methods methods call the automatic
cleanup procedure, RemoveResources(). The DISASSOCIATE method
calls the RemoveResources with the boolean argument set to TRUE,
whereas both the METH_REMOVE and the METH_DESTROY call
RemoveResources() with the boolean argument set to FALSE.
However, process destruction is a two step process, and the first
step is the METH_REMOVE signal. The METH_REMOVE sets a REMOVE
bit in the ShadowProcess flags field (to prevent duplicate
METH_REMOVE methods) and calls the RemoveResources() call.
Process shutdown of a program that has been started by the
InitOOProgram() call, or the CREATE-ASSOCIATE pair of calls,
needs to be shutdown with the RemoveCurrentProgram() call. This
call takes as an argument the optional semaphore that you
started all of the threads with. As stated before, the preferred
manner of starting up is to use the parent process and not a
global program semaphore -- but the option exists for you to
consider. The RemoveCurrentProgram() will not return until it
is safe for your program to exit.
METH_FLAG_OBJECT_AS_DEST
There is a specific method flag to deal with process objects --
specifically the METH_REMOVE method. It is completely reasonable
to wish that a method for a process actually run in that process.
This is the reason for the METH_FLAG_OBJECT_AS_DEST flag for the
mtag_flags field of the METHOD_TAG structure. Specifying the
METH_FLAG_OBJECT_AS_DEST instead of METH_FLAG_OBJECT or
METH_CLASS, makes the destination of the method the same as the
object the method is being invoked on. In short, invoking a
method on a process object, where the method is set with the
METH_OBJECT_AS_DEST flag, forces the method to be run in that
process object.
Initialization Calls
To recap from the PROCESS_CLASS discussion above, a program is
initialized and removed in the following fashion under SHADOW:
if (ShadowBase = OpenLibrary("shadow.library", 5))
{
if (InitOOProgram(NEW_PROCESS_NAME))
{
/*
* Do what you like!
* Probably a call to HandleMessages()
* HandleMessages() is a macro for:
* DoShadow(CurrentProcess(), NULL, METH_PROC_HANDLER,
* METHOD_END);
*/
/*
* Cleanup!
*/
RemoveCurrentProgram(NULL);
}
CloseLibrary(ShadowBase);
}
CONCLUSION
We have finally come to the end of the "Introduction" to SHADOW.
We have come quite far during the course of this document.
Unfortunately, not everything has been covered in the detail
that it really needs to be. In order to better understand
SHADOW, the ShadowLibraryMethods.doc, the ShadowLibraryFuncs.doc,
and the ShadowLibFuncs.doc are highly recommended. The introduction
to ShadowLibraryMethods.doc is recommended regardless.
In addition, you can browse the basic class descriptions of
SHADOW using the browser. In addition, the source to browser,
though somewhat hacked, is included for your use and perusal.
If you have understood all of this -- congratulations! You've done
better than I would have! Try and figure out the included sources,
and contact me with cash for your includes and shadow.lib today!
David C. Navas
SHADOW
7 Avocet Dr. #205
Redwood Shores, CA 94065
jazz@netcom.com
BIX: dnavas